Skip to content

Commit 0cc5678

Browse files
authored
chore(core/protocols): improve body-len checking and awsQueryCompat for schema-serde (#7290)
* chore(core/protocols): improve body-len checking and deduplicate shared code * chore(core/protocols): awsQueryCompat support for schema-serde * chore: unused import
1 parent f560a39 commit 0cc5678

File tree

18 files changed

+604
-206
lines changed

18 files changed

+604
-206
lines changed

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddProtocolConfig.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
package software.amazon.smithy.aws.typescript.codegen;
77

88
import java.util.Collections;
9+
import java.util.List;
910
import java.util.Map;
1011
import java.util.Objects;
1112
import java.util.function.Consumer;
1213
import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait;
1314
import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait;
15+
import software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait;
1416
import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait;
1517
import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait;
1618
import software.amazon.smithy.aws.traits.protocols.RestJson1Trait;
1719
import software.amazon.smithy.aws.traits.protocols.RestXmlTrait;
1820
import software.amazon.smithy.codegen.core.SymbolProvider;
1921
import software.amazon.smithy.model.Model;
2022
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
23+
import software.amazon.smithy.protocol.traits.Rpcv2CborTrait;
2124
import software.amazon.smithy.typescript.codegen.LanguageTarget;
2225
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
2326
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
@@ -60,6 +63,13 @@ public void addConfigInterfaceFields(
6063
// by the smithy client config interface.
6164
}
6265

66+
@Override
67+
public List<String> runAfter() {
68+
return List.of(
69+
software.amazon.smithy.typescript.codegen.integration.AddProtocolConfig.class.getCanonicalName()
70+
);
71+
}
72+
6373
@Override
6474
public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
6575
TypeScriptSettings settings,
@@ -76,6 +86,7 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
7686
.getTrait(XmlNamespaceTrait.class)
7787
.map(XmlNamespaceTrait::getUri)
7888
.orElse("");
89+
String awsQueryCompat = settings.getService(model).hasTrait(AwsQueryCompatibleTrait.class) ? "true" : "false";
7990

8091
switch (target) {
8192
case SHARED:
@@ -148,9 +159,15 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
148159
"AwsJson1_0Protocol", null,
149160
AwsDependency.AWS_SDK_CORE, "/protocols");
150161
writer.write(
151-
"new AwsJson1_0Protocol({ defaultNamespace: $S, serviceTarget: $S })",
162+
"""
163+
new AwsJson1_0Protocol({
164+
defaultNamespace: $S,
165+
serviceTarget: $S,
166+
awsQueryCompatible: $L
167+
})""",
152168
namespace,
153-
rpcTarget
169+
rpcTarget,
170+
awsQueryCompat
154171
);
155172
}
156173
);
@@ -161,9 +178,32 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
161178
"AwsJson1_1Protocol", null,
162179
AwsDependency.AWS_SDK_CORE, "/protocols");
163180
writer.write(
164-
"new AwsJson1_1Protocol({ defaultNamespace: $S, serviceTarget: $S })",
181+
"""
182+
new AwsJson1_1Protocol({
183+
defaultNamespace: $S,
184+
serviceTarget: $S,
185+
awsQueryCompatible: $L
186+
})""",
187+
namespace,
188+
rpcTarget,
189+
awsQueryCompat
190+
);
191+
}
192+
);
193+
} else if (Objects.equals(settings.getProtocol(), Rpcv2CborTrait.ID)) {
194+
return MapUtils.of(
195+
"protocol", writer -> {
196+
writer.addImportSubmodule(
197+
"AwsSmithyRpcV2CborProtocol", null,
198+
AwsDependency.AWS_SDK_CORE, "/protocols");
199+
writer.write(
200+
"""
201+
new AwsSmithyRpcV2CborProtocol({
202+
defaultNamespace: $S,
203+
awsQueryCompatible: $L
204+
})""",
165205
namespace,
166-
rpcTarget
206+
awsQueryCompat
167207
);
168208
}
169209
);
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { ErrorSchema, NormalizedSchema, TypeRegistry } from "@smithy/core/schema";
2+
import type {
3+
BodyLengthCalculator,
4+
HttpResponse as IHttpResponse,
5+
MetadataBearer,
6+
ResponseMetadata,
7+
SerdeFunctions,
8+
} from "@smithy/types";
9+
import { calculateBodyLength } from "@smithy/util-body-length-browser";
10+
11+
/**
12+
* @internal
13+
*/
14+
type ErrorMetadataBearer = MetadataBearer & {
15+
$response: IHttpResponse;
16+
$fault: "client" | "server";
17+
};
18+
19+
/**
20+
* Shared code for Protocols.
21+
*
22+
* @internal
23+
*/
24+
export class ProtocolLib {
25+
/**
26+
* @param body - to be inspected.
27+
* @param serdeContext - this is a subset type but in practice is the client.config having a property called bodyLengthChecker.
28+
*
29+
* @returns content-length value for the body if possible.
30+
* @throws Error and should be caught and handled if not possible to determine length.
31+
*/
32+
public calculateContentLength(body: any, serdeContext?: SerdeFunctions) {
33+
const bodyLengthCalculator: BodyLengthCalculator =
34+
(
35+
serdeContext as SerdeFunctions & {
36+
bodyLengthChecker?: BodyLengthCalculator;
37+
}
38+
)?.bodyLengthChecker ?? calculateBodyLength;
39+
return String(bodyLengthCalculator(body));
40+
}
41+
42+
/**
43+
* This is only for REST protocols.
44+
*
45+
* @param defaultContentType - of the protocol.
46+
* @param inputSchema - schema for which to determine content type.
47+
*
48+
* @returns content-type header value or undefined when not applicable.
49+
*/
50+
public resolveRestContentType(defaultContentType: string, inputSchema: NormalizedSchema): string | undefined {
51+
const members = inputSchema.getMemberSchemas();
52+
const httpPayloadMember = Object.values(members).find((m) => {
53+
return !!m.getMergedTraits().httpPayload;
54+
});
55+
56+
if (httpPayloadMember) {
57+
const mediaType = httpPayloadMember.getMergedTraits().mediaType as string;
58+
if (mediaType) {
59+
return mediaType;
60+
} else if (httpPayloadMember.isStringSchema()) {
61+
return "text/plain";
62+
} else if (httpPayloadMember.isBlobSchema()) {
63+
return "application/octet-stream";
64+
} else {
65+
return defaultContentType;
66+
}
67+
} else if (!inputSchema.isUnitSchema()) {
68+
const hasBody = Object.values(members).find((m) => {
69+
const { httpQuery, httpQueryParams, httpHeader, httpLabel, httpPrefixHeaders } = m.getMergedTraits();
70+
return !httpQuery && !httpQueryParams && !httpHeader && !httpLabel && httpPrefixHeaders === void 0;
71+
});
72+
if (hasBody) {
73+
return defaultContentType;
74+
}
75+
}
76+
}
77+
78+
/**
79+
* Shared code for finding error schema or throwing an unmodeled base error.
80+
* @returns error schema and error metadata.
81+
*
82+
* @throws ServiceBaseException or generic Error if no error schema could be found.
83+
*/
84+
public async getErrorSchemaOrThrowBaseException(
85+
errorIdentifier: string,
86+
defaultNamespace: string,
87+
response: IHttpResponse,
88+
dataObject: any,
89+
metadata: ResponseMetadata,
90+
getErrorSchema?: (registry: TypeRegistry, errorName: string) => ErrorSchema
91+
): Promise<{ errorSchema: ErrorSchema; errorMetadata: ErrorMetadataBearer }> {
92+
let namespace = defaultNamespace;
93+
let errorName = errorIdentifier;
94+
if (errorIdentifier.includes("#")) {
95+
[namespace, errorName] = errorIdentifier.split("#");
96+
}
97+
98+
const errorMetadata: ErrorMetadataBearer = {
99+
$metadata: metadata,
100+
$response: response,
101+
$fault: response.statusCode < 500 ? ("client" as const) : ("server" as const),
102+
};
103+
104+
const registry = TypeRegistry.for(namespace);
105+
106+
try {
107+
const errorSchema = getErrorSchema?.(registry, errorName) ?? (registry.getSchema(errorIdentifier) as ErrorSchema);
108+
return { errorSchema, errorMetadata };
109+
} catch (e) {
110+
if (dataObject.Message) {
111+
dataObject.message = dataObject.Message;
112+
}
113+
const baseExceptionSchema = TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
114+
if (baseExceptionSchema) {
115+
const ErrorCtor = baseExceptionSchema.ctor;
116+
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
117+
}
118+
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
119+
}
120+
}
121+
122+
/**
123+
* Reads the x-amzn-query-error header for awsQuery compatibility.
124+
*
125+
* @param output - values that will be assigned to an error object.
126+
* @param response - from which to read awsQueryError headers.
127+
*/
128+
public setQueryCompatError(output: Record<string, any>, response: IHttpResponse) {
129+
const queryErrorHeader = response.headers?.["x-amzn-query-error"];
130+
131+
if (output !== undefined && queryErrorHeader != null) {
132+
const [Code, Type] = queryErrorHeader.split(";");
133+
const entries = Object.entries(output);
134+
const Error = {
135+
Code,
136+
Type,
137+
} as any;
138+
Object.assign(output, Error);
139+
for (const [k, v] of entries) {
140+
Error[k] = v;
141+
}
142+
delete Error.__type;
143+
output.Error = Error;
144+
}
145+
}
146+
147+
/**
148+
* Assigns Error, Type, Code from the awsQuery error object to the output error object.
149+
* @param queryCompatErrorData - query compat error object.
150+
* @param errorData - canonical error object returned to the caller.
151+
*/
152+
public queryCompatOutput(queryCompatErrorData: any, errorData: any) {
153+
if (queryCompatErrorData.Error) {
154+
errorData.Error = queryCompatErrorData.Error;
155+
}
156+
if (queryCompatErrorData.Type) {
157+
errorData.Type = queryCompatErrorData.Type;
158+
}
159+
if (queryCompatErrorData.Code) {
160+
errorData.Code = queryCompatErrorData.Code;
161+
}
162+
}
163+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { cbor } from "@smithy/core/cbor";
2+
import { op, SCHEMA } from "@smithy/core/schema";
3+
import { error as registerError } from "@smithy/core/schema";
4+
import { HttpResponse } from "@smithy/protocol-http";
5+
import { describe, expect, test as it } from "vitest";
6+
7+
import { AwsSmithyRpcV2CborProtocol } from "./AwsSmithyRpcV2CborProtocol";
8+
9+
describe(AwsSmithyRpcV2CborProtocol.name, () => {
10+
it("should support awsQueryCompatible", async () => {
11+
const protocol = new AwsSmithyRpcV2CborProtocol({
12+
defaultNamespace: "ns",
13+
awsQueryCompatible: true,
14+
});
15+
16+
class MyQueryError extends Error {}
17+
18+
registerError(
19+
"ns",
20+
"MyQueryError",
21+
{ error: "client" },
22+
["Message", "Prop2"],
23+
[SCHEMA.STRING, SCHEMA.NUMERIC],
24+
MyQueryError
25+
);
26+
27+
const body = cbor.serialize({
28+
Message: "oh no",
29+
Prop2: 9999,
30+
});
31+
32+
const error = await (async () => {
33+
return protocol.deserializeResponse(
34+
op("ns", "Operation", 0, "unit", "unit"),
35+
{} as any,
36+
new HttpResponse({
37+
statusCode: 400,
38+
headers: {
39+
"x-amzn-query-error": "MyQueryError;Client",
40+
},
41+
body,
42+
})
43+
);
44+
})().catch((e: any) => e);
45+
46+
expect(error.$metadata).toEqual({
47+
cfId: undefined,
48+
extendedRequestId: undefined,
49+
httpStatusCode: 400,
50+
requestId: undefined,
51+
});
52+
53+
expect(error.$response).toEqual(
54+
new HttpResponse({
55+
body,
56+
headers: {
57+
"x-amzn-query-error": "MyQueryError;Client",
58+
},
59+
reason: undefined,
60+
statusCode: 400,
61+
})
62+
);
63+
64+
expect(error.Code).toEqual(MyQueryError.name);
65+
expect(error.Error.Code).toEqual(MyQueryError.name);
66+
67+
expect(error.Message).toEqual("oh no");
68+
expect(error.Prop2).toEqual(9999);
69+
70+
expect(error.Error.Message).toEqual("oh no");
71+
expect(error.Error.Prop2).toEqual(9999);
72+
73+
expect(error).toMatchObject({
74+
$fault: "client",
75+
Message: "oh no",
76+
message: "oh no",
77+
Prop2: 9999,
78+
Error: {
79+
Code: "MyQueryError",
80+
Message: "oh no",
81+
Type: "Client",
82+
Prop2: 9999,
83+
},
84+
Type: "Client",
85+
Code: "MyQueryError",
86+
});
87+
});
88+
});

0 commit comments

Comments
 (0)