Skip to content

Commit 594cb58

Browse files
committed
test(core/protocols): add test and additional condition for xml declaration
1 parent 309a20f commit 594cb58

File tree

2 files changed

+144
-68
lines changed

2 files changed

+144
-68
lines changed

packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.spec.ts

Lines changed: 122 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
2-
import { StaticOperationSchema } from "@smithy/types";
2+
import { StaticOperationSchema, StaticStructureSchema } from "@smithy/types";
33
import { toUtf8 } from "@smithy/util-utf8";
4-
import { Readable } from "node:stream";
54
import { describe, expect, test as it } from "vitest";
65

76
import { context, deleteObjects } from "../test-schema.spec";
@@ -13,6 +12,11 @@ describe(AwsRestXmlProtocol.name, () => {
1312
schema: deleteObjects,
1413
};
1514

15+
const protocol = new AwsRestXmlProtocol({
16+
xmlNamespace: "http://s3.amazonaws.com/doc/2006-03-01/",
17+
defaultNamespace: "com.amazonaws.s3",
18+
});
19+
1620
describe("serialization", () => {
1721
const testCases = [
1822
{
@@ -59,11 +63,6 @@ describe(AwsRestXmlProtocol.name, () => {
5963

6064
for (const testCase of testCases) {
6165
it(`should serialize HTTP Requests: ${testCase.name}`, async () => {
62-
const protocol = new AwsRestXmlProtocol({
63-
xmlNamespace: "http://s3.amazonaws.com/doc/2006-03-01/",
64-
defaultNamespace: "com.amazonaws.s3",
65-
});
66-
6766
const [, namespace, name, traits, input, output] = command.schema as StaticOperationSchema;
6867

6968
const httpRequest = await protocol.serializeRequest(
@@ -95,74 +94,135 @@ describe(AwsRestXmlProtocol.name, () => {
9594
);
9695
});
9796
}
98-
});
9997

100-
it("deserializes http responses", async () => {
101-
const httpResponse = new HttpResponse({
102-
statusCode: 200,
103-
headers: {},
104-
});
98+
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 () => {
99+
const document = [
100+
3,
101+
"ns",
102+
"Struct",
103+
0,
104+
["a", "b", "c", "h"],
105+
[0, 0, 0, [0, { httpHeader: "content-type" }]],
106+
] satisfies StaticStructureSchema;
107+
const payload = [
108+
3,
109+
"ns",
110+
"PayloadStruct",
111+
0,
112+
["a", "b", "c", "h"],
113+
[0, 0, [0, { httpPayload: 1 }], [0, { httpHeader: "content-type" }]],
114+
] satisfies StaticStructureSchema;
115+
116+
const createOperation = (input: StaticStructureSchema) => ({
117+
namespace: "ns",
118+
name: "operation",
119+
traits: {},
120+
input,
121+
output: "unit" as const,
122+
});
105123

106-
const protocol = new AwsRestXmlProtocol({
107-
defaultNamespace: "",
108-
xmlNamespace: "ns",
109-
});
124+
const httpRequest1 = await protocol.serializeRequest(
125+
createOperation(document),
126+
{
127+
c: "<XML></XML>",
128+
h: "application/xml",
129+
},
130+
context
131+
);
132+
133+
// this is not a payload binding, so although the
134+
// content and header appear to be XML,
135+
// the data is encoded within an XML container structure and the xml declaration
136+
// is prepended by the SDK.
137+
expect(httpRequest1.body).toBe(
138+
`<?xml version="1.0" encoding="UTF-8"?><Struct xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><c>&lt;XML&gt;&lt;/XML&gt;</c></Struct>`
139+
);
140+
141+
const httpRequest2 = await protocol.serializeRequest(
142+
createOperation(payload),
143+
{
144+
c: "<XML></XML>",
145+
h: "application/xml",
146+
},
147+
context
148+
);
110149

111-
const output = await protocol.deserializeResponse(
112-
{
113-
namespace: deleteObjects[1],
114-
name: deleteObjects[2],
115-
traits: deleteObjects[3],
116-
input: deleteObjects[4],
117-
output: deleteObjects[5],
118-
},
119-
context,
120-
httpResponse
121-
);
122-
123-
expect(output).toEqual({
124-
$metadata: {
125-
httpStatusCode: 200,
126-
requestId: undefined,
127-
extendedRequestId: undefined,
128-
cfId: undefined,
129-
},
130-
});
131-
});
150+
// even though this could be interpreted as XML input by the content and the header value,
151+
// because this operation is a payload binding of a string,
152+
// we simply send what the caller has provided rather than prepending the XML declaration.
153+
expect(httpRequest2.body).toBe(`<XML></XML>`);
132154

133-
it("decorates service exceptions with unmodeled fields", async () => {
134-
const httpResponse = new HttpResponse({
135-
statusCode: 400,
136-
headers: {},
137-
body: Buffer.from(`<Exception><UnmodeledField>Oh no</UnmodeledField></Exception>`),
138-
});
155+
const httpRequest3 = await protocol.serializeRequest(
156+
createOperation(payload),
157+
{
158+
c: "<XML></XML>",
159+
h: "text/xml",
160+
},
161+
context
162+
);
139163

140-
const protocol = new AwsRestXmlProtocol({
141-
defaultNamespace: "",
142-
xmlNamespace: "ns",
164+
expect(httpRequest3.body).toBe(`<XML></XML>`);
143165
});
166+
});
144167

145-
const output = await protocol
146-
.deserializeResponse(
168+
describe("deserialization", () => {
169+
it("deserializes http responses", async () => {
170+
const httpResponse = new HttpResponse({
171+
statusCode: 200,
172+
headers: {},
173+
});
174+
175+
const output = await protocol.deserializeResponse(
147176
{
148-
namespace: "ns",
149-
name: "Empty",
150-
traits: 0,
151-
input: "unit" as const,
152-
output: [3, "ns", "EmptyOutput", 0, [], []],
177+
namespace: deleteObjects[1],
178+
name: deleteObjects[2],
179+
traits: deleteObjects[3],
180+
input: deleteObjects[4],
181+
output: deleteObjects[5],
153182
},
154183
context,
155184
httpResponse
156-
)
157-
.catch((e) => {
158-
return e;
185+
);
186+
187+
expect(output).toEqual({
188+
$metadata: {
189+
httpStatusCode: 200,
190+
requestId: undefined,
191+
extendedRequestId: undefined,
192+
cfId: undefined,
193+
},
194+
});
195+
});
196+
197+
it("decorates service exceptions with unmodeled fields", async () => {
198+
const httpResponse = new HttpResponse({
199+
statusCode: 400,
200+
headers: {},
201+
body: Buffer.from(`<Exception><UnmodeledField>Oh no</UnmodeledField></Exception>`),
159202
});
160203

161-
expect(output).toMatchObject({
162-
UnmodeledField: "Oh no",
163-
$metadata: {
164-
httpStatusCode: 400,
165-
},
204+
const output = await protocol
205+
.deserializeResponse(
206+
{
207+
namespace: "ns",
208+
name: "Empty",
209+
traits: 0,
210+
input: "unit" as const,
211+
output: [3, "ns", "EmptyOutput", 0, [], []],
212+
},
213+
context,
214+
httpResponse
215+
)
216+
.catch((e) => {
217+
return e;
218+
});
219+
220+
expect(output).toMatchObject({
221+
UnmodeledField: "Oh no",
222+
$metadata: {
223+
httpStatusCode: 400,
224+
},
225+
});
166226
});
167227
});
168228
});

packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,17 @@ export class AwsRestXmlProtocol extends HttpBindingProtocol {
7070
}
7171
}
7272

73-
if (request.headers["content-type"] === this.getDefaultContentType()) {
74-
if (typeof request.body === "string") {
75-
if (!request.body.startsWith("<?xml ")) {
76-
request.body = '<?xml version="1.0" encoding="UTF-8"?>' + request.body;
77-
}
78-
}
73+
if (
74+
typeof request.body === "string" &&
75+
request.headers["content-type"] === this.getDefaultContentType() &&
76+
!request.body.startsWith("<?xml ") &&
77+
!this.hasUnstructuredPayloadBinding(inputSchema)
78+
) {
79+
// string type excludes event streams.
80+
// if the body is a string, it is either XML serialized from a structure
81+
// or a text payload. We exclude text payloads by checking
82+
// whether the schema has a payload binding.
83+
request.body = '<?xml version="1.0" encoding="UTF-8"?>' + request.body;
7984
}
8085

8186
// content-length header is set by the contentLengthMiddleware.
@@ -145,4 +150,15 @@ export class AwsRestXmlProtocol extends HttpBindingProtocol {
145150
protected getDefaultContentType(): string {
146151
return "application/xml";
147152
}
153+
154+
private hasUnstructuredPayloadBinding(ns: NormalizedSchema): boolean {
155+
for (const [, member] of ns.structIterator()) {
156+
if (member.getMergedTraits().httpPayload) {
157+
// all struct members can be http payloads, but the serialization of
158+
// simple types are probably not in XML format.
159+
return !(member.isStructSchema() || member.isMapSchema() || member.isListSchema());
160+
}
161+
}
162+
return false;
163+
}
148164
}

0 commit comments

Comments
 (0)