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 | 782 | binaryBody?.ok === true && !octetStreamContent && isOctetStream(rb.contentType) |
| 783 | 783 | ? rb.contentType |
| 784 | 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 | 795 | return yield* new OpenApiInvocationError({ |
| 787 | 796 | message: "application/octet-stream request body must be bytes; provide `bodyBase64`", |
| 788 | 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 | 372 | Effect.gen(function* () { |
| 373 | 373 | const { server, captured } = yield* startEchoServer({ |
| 374 | 374 | payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()), |
| @@ -380,15 +380,16 @@ describe("OpenAPI non-JSON request body dispatch", () => { · 5 unmodified lines | ||
| 380 | 380 | slug: "bin_string_body", |
| 381 | 381 | }); |
| 382 | 382 | |
| 383 | − const exit = yield* executor | |
| 384 | − .execute(conn.address("body.submit"), { | |
| 385 | − body: "3q2+7w==", | |
| 386 | − }) | |
| 387 | − .pipe(Effect.exit); | |
| 383 | + // A plain string `body` is the long-standing way callers upload text | |
| 384 | + // content to an octet-stream endpoint, so it must reach the wire rather | |
| 385 | + // than failing. It is sent verbatim as UTF-8 bytes (not base64-decoded), | |
| 386 | + // which preserves the pre-bodyBase64 behavior; binary uploads still use | |
| 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); | |
| 390 | − expect(captured.contentType).toBe(""); | |
| 391 | − expect(captured.body.length).toBe(0); | |
| 391 | + expect(captured.contentType).toBe("application/octet-stream"); | |
| 392 | + expect(captured.body.toString("utf8")).toBe(text); | |
| 392 | 393 | }), |
| 393 | 394 | ); |
| 394 | 395 | |