Skip to content
Merged
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
184 changes: 122 additions & 62 deletions packages/core/src/submodules/protocols/xml/AwsRestXmlProtocol.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = [
{
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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: "<XML></XML>",
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 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>`
);

const httpRequest2 = await protocol.serializeRequest(
createOperation(payload),
{
c: "<XML></XML>",
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(`<XML></XML>`);

it("decorates service exceptions with unmodeled fields", async () => {
const httpResponse = new HttpResponse({
statusCode: 400,
headers: {},
body: Buffer.from(`<Exception><UnmodeledField>Oh no</UnmodeledField></Exception>`),
});
const httpRequest3 = await protocol.serializeRequest(
createOperation(payload),
{
c: "<XML></XML>",
h: "text/xml",
},
context
);

const protocol = new AwsRestXmlProtocol({
defaultNamespace: "",
xmlNamespace: "ns",
expect(httpRequest3.body).toBe(`<XML></XML>`);
});
});

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(`<Exception><UnmodeledField>Oh no</UnmodeledField></Exception>`),
});

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,
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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("<?xml ")) {
request.body = '<?xml version="1.0" encoding="UTF-8"?>' + request.body;
}
}
if (
typeof request.body === "string" &&
request.headers["content-type"] === this.getDefaultContentType() &&
!request.body.startsWith("<?xml ") &&
!this.hasUnstructuredPayloadBinding(inputSchema)
) {
// string type excludes event streams.
// if the body is a string, it is either XML serialized from a structure
// or a text payload. We exclude text payloads by checking
// whether the schema has a payload binding.
request.body = '<?xml version="1.0" encoding="UTF-8"?>' + request.body;
}

// content-length header is set by the contentLengthMiddleware.
Expand Down Expand Up @@ -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;
}
}
Loading