Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
36 changes: 36 additions & 0 deletions packages/http/src/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -745,3 +764,20 @@ 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") {
return true;
}
current = current.baseScalar;
}
return false;
}
52 changes: 52 additions & 0 deletions packages/http/test/parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down