diff --git a/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.spec.ts b/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.spec.ts index d4784c85b660..3726786edd16 100644 --- a/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.spec.ts +++ b/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.spec.ts @@ -1,7 +1,6 @@ import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; -import { StaticOperationSchema } from "@smithy/types"; +import { StaticOperationSchema, StaticStructureSchema } from "@smithy/types"; import { toUtf8 } from "@smithy/util-utf8"; -import { Readable } from "node:stream"; import { describe, expect, test as it } from "vitest"; import { context, deleteObjects } from "../test-schema.spec"; @@ -13,6 +12,11 @@ describe(AwsRestXmlProtocol.name, () => { schema: deleteObjects, }; + const protocol = new AwsRestXmlProtocol({ + xmlNamespace: "http://s3.amazonaws.com/doc/2006-03-01/", + defaultNamespace: "com.amazonaws.s3", + }); + describe("serialization", () => { const testCases = [ { @@ -59,11 +63,6 @@ describe(AwsRestXmlProtocol.name, () => { for (const testCase of testCases) { it(`should serialize HTTP Requests: ${testCase.name}`, async () => { - const protocol = new AwsRestXmlProtocol({ - xmlNamespace: "http://s3.amazonaws.com/doc/2006-03-01/", - defaultNamespace: "com.amazonaws.s3", - }); - const [, namespace, name, traits, input, output] = command.schema as StaticOperationSchema; const httpRequest = await protocol.serializeRequest( @@ -95,74 +94,135 @@ describe(AwsRestXmlProtocol.name, () => { ); }); } - }); - it("deserializes http responses", async () => { - const httpResponse = new HttpResponse({ - statusCode: 200, - headers: {}, - }); + it("should prepend an xml declaration only if the content type is application/xml and the input schema does not have a payload binding", async () => { + const document = [ + 3, + "ns", + "Struct", + 0, + ["a", "b", "c", "h"], + [0, 0, 0, [0, { httpHeader: "content-type" }]], + ] satisfies StaticStructureSchema; + const payload = [ + 3, + "ns", + "PayloadStruct", + 0, + ["a", "b", "c", "h"], + [0, 0, [0, { httpPayload: 1 }], [0, { httpHeader: "content-type" }]], + ] satisfies StaticStructureSchema; + + const createOperation = (input: StaticStructureSchema) => ({ + namespace: "ns", + name: "operation", + traits: {}, + input, + output: "unit" as const, + }); - const protocol = new AwsRestXmlProtocol({ - defaultNamespace: "", - xmlNamespace: "ns", - }); + const httpRequest1 = await protocol.serializeRequest( + createOperation(document), + { + c: "", + h: "application/xml", + }, + context + ); + + // this is not a payload binding, so although the + // content and header appear to be XML, + // the data is encoded within an XML container structure and the xml declaration + // is prepended by the SDK. + expect(httpRequest1.body).toBe( + `<XML></XML>` + ); + + const httpRequest2 = await protocol.serializeRequest( + createOperation(payload), + { + c: "", + h: "application/xml", + }, + context + ); - const output = await protocol.deserializeResponse( - { - namespace: deleteObjects[1], - name: deleteObjects[2], - traits: deleteObjects[3], - input: deleteObjects[4], - output: deleteObjects[5], - }, - context, - httpResponse - ); - - expect(output).toEqual({ - $metadata: { - httpStatusCode: 200, - requestId: undefined, - extendedRequestId: undefined, - cfId: undefined, - }, - }); - }); + // even though this could be interpreted as XML input by the content and the header value, + // because this operation is a payload binding of a string, + // we simply send what the caller has provided rather than prepending the XML declaration. + expect(httpRequest2.body).toBe(``); - it("decorates service exceptions with unmodeled fields", async () => { - const httpResponse = new HttpResponse({ - statusCode: 400, - headers: {}, - body: Buffer.from(`Oh no`), - }); + const httpRequest3 = await protocol.serializeRequest( + createOperation(payload), + { + c: "", + h: "text/xml", + }, + context + ); - const protocol = new AwsRestXmlProtocol({ - defaultNamespace: "", - xmlNamespace: "ns", + expect(httpRequest3.body).toBe(``); }); + }); - const output = await protocol - .deserializeResponse( + describe("deserialization", () => { + it("deserializes http responses", async () => { + const httpResponse = new HttpResponse({ + statusCode: 200, + headers: {}, + }); + + const output = await protocol.deserializeResponse( { - namespace: "ns", - name: "Empty", - traits: 0, - input: "unit" as const, - output: [3, "ns", "EmptyOutput", 0, [], []], + namespace: deleteObjects[1], + name: deleteObjects[2], + traits: deleteObjects[3], + input: deleteObjects[4], + output: deleteObjects[5], }, context, httpResponse - ) - .catch((e) => { - return e; + ); + + expect(output).toEqual({ + $metadata: { + httpStatusCode: 200, + requestId: undefined, + extendedRequestId: undefined, + cfId: undefined, + }, + }); + }); + + it("decorates service exceptions with unmodeled fields", async () => { + const httpResponse = new HttpResponse({ + statusCode: 400, + headers: {}, + body: Buffer.from(`Oh no`), }); - expect(output).toMatchObject({ - UnmodeledField: "Oh no", - $metadata: { - httpStatusCode: 400, - }, + const output = await protocol + .deserializeResponse( + { + namespace: "ns", + name: "Empty", + traits: 0, + input: "unit" as const, + output: [3, "ns", "EmptyOutput", 0, [], []], + }, + context, + httpResponse + ) + .catch((e) => { + return e; + }); + + expect(output).toMatchObject({ + UnmodeledField: "Oh no", + $metadata: { + httpStatusCode: 400, + }, + }); }); }); }); diff --git a/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.ts b/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.ts index 458fcb2f007d..d5e85a97d9e9 100644 --- a/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.ts +++ b/packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.ts @@ -70,12 +70,17 @@ export class AwsRestXmlProtocol extends HttpBindingProtocol { } } - if (request.headers["content-type"] === this.getDefaultContentType()) { - if (typeof request.body === "string") { - if (!request.body.startsWith("' + request.body; - } - } + if ( + typeof request.body === "string" && + request.headers["content-type"] === this.getDefaultContentType() && + !request.body.startsWith("' + request.body; } // content-length header is set by the contentLengthMiddleware. @@ -145,4 +150,15 @@ export class AwsRestXmlProtocol extends HttpBindingProtocol { protected getDefaultContentType(): string { return "application/xml"; } + + private hasUnstructuredPayloadBinding(ns: NormalizedSchema): boolean { + for (const [, member] of ns.structIterator()) { + if (member.getMergedTraits().httpPayload) { + // all struct members can be http payloads, but the serialization of + // simple types are probably not in XML format. + return !(member.isStructSchema() || member.isMapSchema() || member.isListSchema()); + } + } + return false; + } }