Skip to content

Commit 76b6f5a

Browse files
committed
chore(core/protocols): improve body-len checking and deduplicate shared code
1 parent 139aa8d commit 76b6f5a

File tree

5 files changed

+180
-180
lines changed

5 files changed

+180
-180
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
}

packages/core/src/submodules/protocols/json/AwsJsonRpcProtocol.ts

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RpcProtocol } from "@smithy/core/protocols";
2-
import { deref, ErrorSchema, NormalizedSchema, SCHEMA, TypeRegistry } from "@smithy/core/schema";
3-
import {
2+
import { deref, NormalizedSchema, SCHEMA } from "@smithy/core/schema";
3+
import type {
44
EndpointBearer,
55
HandlerExecutionContext,
66
HttpRequest,
@@ -11,8 +11,8 @@ import {
1111
ShapeDeserializer,
1212
ShapeSerializer,
1313
} from "@smithy/types";
14-
import { calculateBodyLength } from "@smithy/util-body-length-browser";
1514

15+
import { ProtocolLib } from "../ProtocolLib";
1616
import { JsonCodec } from "./JsonCodec";
1717
import { loadRestJsonErrorCode } from "./parseJsonBody";
1818

@@ -23,7 +23,8 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
2323
protected serializer: ShapeSerializer<string | Uint8Array>;
2424
protected deserializer: ShapeDeserializer<string | Uint8Array>;
2525
protected serviceTarget: string;
26-
private codec: JsonCodec;
26+
private readonly codec: JsonCodec;
27+
private readonly mixin = new ProtocolLib();
2728

2829
protected constructor({ defaultNamespace, serviceTarget }: { defaultNamespace: string; serviceTarget: string }) {
2930
super({
@@ -58,7 +59,7 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
5859
request.body = "{}";
5960
}
6061
try {
61-
request.headers["content-length"] = String(calculateBodyLength(request.body));
62+
request.headers["content-length"] = this.mixin.calculateContentLength(request.body, this.serdeContext);
6263
} catch (e) {}
6364
return request;
6465
}
@@ -79,33 +80,13 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
7980
// loadRestJsonErrorCode is still used in JSON RPC.
8081
const errorIdentifier = loadRestJsonErrorCode(response, dataObject) ?? "Unknown";
8182

82-
let namespace = this.options.defaultNamespace;
83-
let errorName = errorIdentifier;
84-
if (errorIdentifier.includes("#")) {
85-
[namespace, errorName] = errorIdentifier.split("#");
86-
}
87-
88-
const errorMetadata = {
89-
$metadata: metadata,
90-
$response: response,
91-
$fault: response.statusCode <= 500 ? ("client" as const) : ("server" as const),
92-
};
93-
94-
const registry = TypeRegistry.for(namespace);
95-
let errorSchema: ErrorSchema;
96-
try {
97-
errorSchema = registry.getSchema(errorIdentifier) as ErrorSchema;
98-
} catch (e) {
99-
if (dataObject.Message) {
100-
dataObject.message = dataObject.Message;
101-
}
102-
const baseExceptionSchema = TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
103-
if (baseExceptionSchema) {
104-
const ErrorCtor = baseExceptionSchema.ctor;
105-
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
106-
}
107-
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
108-
}
83+
const { errorSchema, errorMetadata } = await this.mixin.getErrorSchemaOrThrowBaseException(
84+
errorIdentifier,
85+
this.options.defaultNamespace,
86+
response,
87+
dataObject,
88+
metadata
89+
);
10990

11091
const ns = NormalizedSchema.of(errorSchema);
11192
const message = dataObject.message ?? dataObject.Message ?? "Unknown";

packages/core/src/submodules/protocols/json/AwsRestJsonProtocol.ts

Lines changed: 15 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
HttpInterceptingShapeDeserializer,
44
HttpInterceptingShapeSerializer,
55
} from "@smithy/core/protocols";
6-
import { ErrorSchema, NormalizedSchema, SCHEMA, TypeRegistry } from "@smithy/core/schema";
7-
import {
6+
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
7+
import type {
88
EndpointBearer,
99
HandlerExecutionContext,
1010
HttpRequest,
@@ -15,8 +15,8 @@ import {
1515
ShapeDeserializer,
1616
ShapeSerializer,
1717
} from "@smithy/types";
18-
import { calculateBodyLength } from "@smithy/util-body-length-browser";
1918

19+
import { ProtocolLib } from "../ProtocolLib";
2020
import { JsonCodec, JsonSettings } from "./JsonCodec";
2121
import { loadRestJsonErrorCode } from "./parseJsonBody";
2222

@@ -27,6 +27,7 @@ export class AwsRestJsonProtocol extends HttpBindingProtocol {
2727
protected serializer: ShapeSerializer<string | Uint8Array>;
2828
protected deserializer: ShapeDeserializer<string | Uint8Array>;
2929
private readonly codec: JsonCodec;
30+
private readonly mixin = new ProtocolLib();
3031

3132
public constructor({ defaultNamespace }: { defaultNamespace: string }) {
3233
super({
@@ -65,32 +66,11 @@ export class AwsRestJsonProtocol extends HttpBindingProtocol {
6566
): Promise<HttpRequest> {
6667
const request = await super.serializeRequest(operationSchema, input, context);
6768
const inputSchema = NormalizedSchema.of(operationSchema.input);
68-
const members = inputSchema.getMemberSchemas();
6969

7070
if (!request.headers["content-type"]) {
71-
const httpPayloadMember = Object.values(members).find((m) => {
72-
return !!m.getMergedTraits().httpPayload;
73-
});
74-
75-
if (httpPayloadMember) {
76-
const mediaType = httpPayloadMember.getMergedTraits().mediaType as string;
77-
if (mediaType) {
78-
request.headers["content-type"] = mediaType;
79-
} else if (httpPayloadMember.isStringSchema()) {
80-
request.headers["content-type"] = "text/plain";
81-
} else if (httpPayloadMember.isBlobSchema()) {
82-
request.headers["content-type"] = "application/octet-stream";
83-
} else {
84-
request.headers["content-type"] = this.getDefaultContentType();
85-
}
86-
} else if (!inputSchema.isUnitSchema()) {
87-
const hasBody = Object.values(members).find((m) => {
88-
const { httpQuery, httpQueryParams, httpHeader, httpLabel, httpPrefixHeaders } = m.getMergedTraits();
89-
return !httpQuery && !httpQueryParams && !httpHeader && !httpLabel && httpPrefixHeaders === void 0;
90-
});
91-
if (hasBody) {
92-
request.headers["content-type"] = this.getDefaultContentType();
93-
}
71+
const contentType = this.mixin.resolveRestContentType(this.getDefaultContentType(), inputSchema);
72+
if (contentType) {
73+
request.headers["content-type"] = contentType;
9474
}
9575
}
9676

@@ -100,8 +80,7 @@ export class AwsRestJsonProtocol extends HttpBindingProtocol {
10080

10181
if (request.body) {
10282
try {
103-
// todo(schema): use config.bodyLengthChecker or move that into serdeContext.
104-
request.headers["content-length"] = String(calculateBodyLength(request.body));
83+
request.headers["content-length"] = this.mixin.calculateContentLength(request.body, this.serdeContext);
10584
} catch (e) {}
10685
}
10786

@@ -117,33 +96,13 @@ export class AwsRestJsonProtocol extends HttpBindingProtocol {
11796
): Promise<never> {
11897
const errorIdentifier = loadRestJsonErrorCode(response, dataObject) ?? "Unknown";
11998

120-
let namespace = this.options.defaultNamespace;
121-
let errorName = errorIdentifier;
122-
if (errorIdentifier.includes("#")) {
123-
[namespace, errorName] = errorIdentifier.split("#");
124-
}
125-
126-
const errorMetadata = {
127-
$metadata: metadata,
128-
$response: response,
129-
$fault: response.statusCode <= 500 ? ("client" as const) : ("server" as const),
130-
};
131-
132-
const registry = TypeRegistry.for(namespace);
133-
let errorSchema: ErrorSchema;
134-
try {
135-
errorSchema = registry.getSchema(errorIdentifier) as ErrorSchema;
136-
} catch (e) {
137-
if (dataObject.Message) {
138-
dataObject.message = dataObject.Message;
139-
}
140-
const baseExceptionSchema = TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
141-
if (baseExceptionSchema) {
142-
const ErrorCtor = baseExceptionSchema.ctor;
143-
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
144-
}
145-
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
146-
}
99+
const { errorSchema, errorMetadata } = await this.mixin.getErrorSchemaOrThrowBaseException(
100+
errorIdentifier,
101+
this.options.defaultNamespace,
102+
response,
103+
dataObject,
104+
metadata
105+
);
147106

148107
const ns = NormalizedSchema.of(errorSchema);
149108
const message = dataObject.message ?? dataObject.Message ?? "Unknown";

0 commit comments

Comments
 (0)