From 128caac90d036e695f41b1ce0b2f64ad01a5cff7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:04:20 +0000 Subject: [PATCH 1/3] Initial plan From 374b60ba77d25147615c92a94d36d00b6be8f881 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:20:33 +0000 Subject: [PATCH 2/3] feat(http): add warning when bytes body is used with XML content type Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/http/src/lib.ts | 6 ++++ packages/http/src/payload.ts | 40 +++++++++++++++++++++ packages/http/test/parameters.test.ts | 52 +++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index b67083b9e39..de2a9523e5f 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -214,6 +214,12 @@ export const $lib = createTypeSpecLibrary({ default: paramMessage`The MergePatch transform does not operate on http envelope metadata. Remove any http metadata decorators ('@query', '@header', '@path', '@cookie', '@statusCode') from the model passed to the MergePatch template. Found '${"metadataType"}' decorating property '${"propertyName"}'`, }, }, + "bytes-xml-body": { + severity: "warning", + messages: { + default: paramMessage`Using 'bytes' as the body type with XML content type '${"contentType"}' will treat the payload as raw binary data rather than structured XML. Use a model type for structured XML serialization.`, + }, + }, }, state: { authentication: { description: "State for the @auth decorator" }, diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts index d69d68b57a2..08957d64730 100644 --- a/packages/http/src/payload.ts +++ b/packages/http/src/payload.ts @@ -93,6 +93,25 @@ export function resolveHttpPayload( ); return diagnostics.wrap({ body: undefined, metadata }); } + + if ( + body.bodyKind === "single" && + body.type.kind === "Scalar" && + isScalarExtendsBytes(body.type) + ) { + for (const ct of body.contentTypes) { + if (isXmlContentType(ct)) { + diagnostics.add( + createDiagnostic({ + code: "bytes-xml-body", + target: body.property ?? type, + format: { contentType: ct }, + }), + ); + break; + } + } + } } return diagnostics.wrap({ body, metadata }); @@ -745,3 +764,24 @@ function resolveContentTypesForBody( } } } + +const xmlContentTypeRegex = /^(application|text)\/(.+\+)?xml$/; + +function isXmlContentType(contentType: string): boolean { + return xmlContentTypeRegex.test(contentType); +} + +function isScalarExtendsBytes(type: Scalar): boolean { + let current: Scalar | undefined = type; + while (current) { + if ( + current.name === "bytes" && + current.namespace?.name === "TypeSpec" && + current.namespace?.namespace?.name === "" + ) { + return true; + } + current = current.baseScalar; + } + return false; +} diff --git a/packages/http/test/parameters.test.ts b/packages/http/test/parameters.test.ts index 91d144cfd6b..95de0da6e7b 100644 --- a/packages/http/test/parameters.test.ts +++ b/packages/http/test/parameters.test.ts @@ -154,6 +154,58 @@ it("emit warning if using contentType parameter without a body", async () => { }); }); +it("emit warning if bytes body has application/xml content type", async () => { + const [_, diagnostics] = await compileOperations(` + @post op post(@header contentType: "application/xml", @body body: bytes): void; + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/http/bytes-xml-body", + severity: "warning", + }); +}); + +it("emit warning if bytes body has text/xml content type", async () => { + const [_, diagnostics] = await compileOperations(` + @post op post(@header contentType: "text/xml", @body body: bytes): void; + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/http/bytes-xml-body", + severity: "warning", + }); +}); + +it("emit warning if bytes body has application/soap+xml content type", async () => { + const [_, diagnostics] = await compileOperations(` + @post op post(@header contentType: "application/soap+xml", @body body: bytes): void; + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/http/bytes-xml-body", + severity: "warning", + }); +}); + +it("emit warning if bytes response body has application/xml content type", async () => { + const [_, diagnostics] = await compileOperations(` + @get op get(): { @header contentType: "application/xml", @body body: bytes }; + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/http/bytes-xml-body", + severity: "warning", + }); +}); + +it("does not emit warning for bytes body with non-xml content type", async () => { + const [_, diagnostics] = await compileOperations(` + @post op post(@header contentType: "application/octet-stream", @body body: bytes): void; + `); + + expectDiagnosticEmpty(diagnostics); +}); + it("resolve body when defined with @body", async () => { const [routes, diagnostics] = await compileOperations(` @get op get(@query select: string, @body bodyParam: string): string; From 41c4e46ff0432725ae6298c011e86acd08d207af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:22:47 +0000 Subject: [PATCH 3/3] simplify isScalarExtendsBytes to match existing codebase pattern Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- ...opilot-fix-xml-bytes-serialization-2026-2-9-19-20-52.md | 7 +++++++ packages/http/src/payload.ts | 6 +----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 .chronus/changes/copilot-fix-xml-bytes-serialization-2026-2-9-19-20-52.md diff --git a/.chronus/changes/copilot-fix-xml-bytes-serialization-2026-2-9-19-20-52.md b/.chronus/changes/copilot-fix-xml-bytes-serialization-2026-2-9-19-20-52.md new file mode 100644 index 00000000000..2baaa4f1092 --- /dev/null +++ b/.chronus/changes/copilot-fix-xml-bytes-serialization-2026-2-9-19-20-52.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http" +--- + +Add warning diagnostic when `bytes` is used as a body type with an XML content type (e.g. `application/xml`). The payload will be treated as raw binary data; use a model type for structured XML serialization. \ No newline at end of file diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts index 08957d64730..492992d79bd 100644 --- a/packages/http/src/payload.ts +++ b/packages/http/src/payload.ts @@ -774,11 +774,7 @@ function isXmlContentType(contentType: string): boolean { function isScalarExtendsBytes(type: Scalar): boolean { let current: Scalar | undefined = type; while (current) { - if ( - current.name === "bytes" && - current.namespace?.name === "TypeSpec" && - current.namespace?.namespace?.name === "" - ) { + if (current.name === "bytes") { return true; } current = current.baseScalar;