A plain string `body` was being rejected for octet-stream operations with "request body must be bytes; provide bodyBase64", which broke existing callers (such as the Microsoft Graph drive item content upload) that send text content as a string. The request layer already handles a string body, so let it through as UTF-8 bytes. Binary uploads still use bodyBase64.
Unified · Split
| @@ -0,0 +1,5 @@ | |||
| 1 | --- | ||
| 2 | "@executor-js/plugin-openapi": patch | ||
| 3 | --- | ||
| 4 | |||
| 5 | Allow a plain string `body` for octet-stream uploads again. Operations like Microsoft Graph's drive item content upload were rejecting string bodies with "request body must be bytes; provide bodyBase64", even though the request layer already sends a string through fine. String bodies now go through as UTF-8 bytes; binary content still uses `bodyBase64`. | ||
| @@ -782,7 +782,16 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 781 unmodified lines | |||
| 782 | binaryBody?.ok === true && !octetStreamContent && isOctetStream(rb.contentType) | 782 | binaryBody?.ok === true && !octetStreamContent && isOctetStream(rb.contentType) |
| 783 | ? rb.contentType | 783 | ? rb.contentType |
| 784 | : (selected?.contentType ?? rb.contentType); | 784 | : (selected?.contentType ?? rb.contentType); |
| 785 | if (isOctetStream(chosenCt) && toUint8Array(bodyValue) === null) { | 785 | // A `bodyBase64` arg already decoded to bytes above. A plain string |
| 786 | // `body` is the long-standing way callers pass (text) octet-stream | ||
| 787 | // upload content, and applyRequestBody sends it through as-is, so let it | ||
| 788 | // past. Only a genuinely non-byte, non-string shape (an object, say) | ||
| 789 | // can't be uploaded as octet-stream and needs `bodyBase64`. | ||
| 790 | if ( | ||
| 791 | isOctetStream(chosenCt) && | ||
| 792 | typeof bodyValue !== "string" && | ||
| 793 | toUint8Array(bodyValue) === null | ||
| 794 | ) { | ||
| 786 | return yield* new OpenApiInvocationError({ | 795 | return yield* new OpenApiInvocationError({ |
| 787 | message: "application/octet-stream request body must be bytes; provide `bodyBase64`", | 796 | message: "application/octet-stream request body must be bytes; provide `bodyBase64`", |
| 788 | statusCode: Option.none(), | 797 | statusCode: Option.none(), |
| @@ -368,7 +368,7 @@ describe("OpenAPI non-JSON request body dispatch", () => { · 367 unmodified lines | |||
| 368 | }), | 368 | }), |
| 369 | ); | 369 | ); |
| 370 | 370 | ||
| 371 | it.effect("application/octet-stream: string body fails before dispatch", () => | 371 | it.effect("application/octet-stream: string body passes through as UTF-8 bytes", () => |
| 372 | Effect.gen(function* () { | 372 | Effect.gen(function* () { |
| 373 | const { server, captured } = yield* startEchoServer({ | 373 | const { server, captured } = yield* startEchoServer({ |
| 374 | payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()), | 374 | payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()), |
| @@ -380,15 +380,16 @@ describe("OpenAPI non-JSON request body dispatch", () => { · 5 unmodified lines | |||
| 380 | slug: "bin_string_body", | 380 | slug: "bin_string_body", |
| 381 | }); | 381 | }); |
| 382 | 382 | ||
| 383 | const exit = yield* executor | 383 | // A plain string `body` is the long-standing way callers upload text |
| 384 | .execute(conn.address("body.submit"), { | 384 | // content to an octet-stream endpoint, so it must reach the wire rather |
| 385 | body: "3q2+7w==", | 385 | // than failing. It is sent verbatim as UTF-8 bytes (not base64-decoded), |
| 386 | }) | 386 | // which preserves the pre-bodyBase64 behavior; binary uploads still use |
| 387 | .pipe(Effect.exit); | 387 | // `bodyBase64`. |
| 388 | const text = "plain file contents, not base64"; | ||
| 389 | yield* executor.execute(conn.address("body.submit"), { body: text }); | ||
| 388 | 390 | ||
| 389 | expect(Exit.isFailure(exit)).toBe(true); | 391 | expect(captured.contentType).toBe("application/octet-stream"); |
| 390 | expect(captured.contentType).toBe(""); | 392 | expect(captured.body.toString("utf8")).toBe(text); |
| 391 | expect(captured.body.length).toBe(0); | ||
| 392 | }), | 393 | }), |
| 393 | ); | 394 | ); |
| 394 | 395 | ||