Skip to content

Commit 612f03e

Browse files
committed
fix(openapi): accept a string body for octet-stream uploads
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.
1 parent 529b908 commit 612f03e

3 files changed

Lines changed: 25 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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`.

packages/plugins/openapi/src/sdk/invoke.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,16 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* (
782782
binaryBody?.ok === true && !octetStreamContent && isOctetStream(rb.contentType)
783783
? rb.contentType
784784
: (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+
) {
786795
return yield* new OpenApiInvocationError({
787796
message: "application/octet-stream request body must be bytes; provide `bodyBase64`",
788797
statusCode: Option.none(),

packages/plugins/openapi/src/sdk/non-json-body.test.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ describe("OpenAPI non-JSON request body dispatch", () => {
368368
}),
369369
);
370370

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", () =>
372372
Effect.gen(function* () {
373373
const { server, captured } = yield* startEchoServer({
374374
payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
@@ -380,15 +380,16 @@ describe("OpenAPI non-JSON request body dispatch", () => {
380380
slug: "bin_string_body",
381381
});
382382

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 });
388390

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);
392393
}),
393394
);
394395

0 commit comments

Comments
 (0)