Skip to content

Commit d9d24b0

Browse files
authored
feat(protocol-http): implement SRA HttpRequest (#4514)
This change implements the core functionality of HttpRequest for SRA. New properties were added to the HttpRequest class, old properties and interfaces were deprecated. Proxy was added for headers and query properties to maintain backward compatibility. This change does not include updates to the SDK to use new functionality. Notes on other changes: * middleware-host-header and middleware-sdk-transcribe-streaming tests: URL.port always returns '' if port is set to the default for the protocol. * middleware-sdk-transcribe-streaming: Providing URL.hostname with a string containing the port number does not update the port. * signature-v4 prepareRequest: See comment in code. * Field: toString method was changed to only require double quotes on values with commas. Current tests allow spaces without double quotes. * Fields: Helper methods were added for getting all fields and creating a Fields object. Additional notes: * When converting between single string header values and a list of field values, split(",") and join(",") are used to avoid making any modifications to the original value, preserving compatibility. * HttpRequest.isInstance was not modified, because doing so might break compatibility. The check was not looking for the clone method, so wasn't actually verifying the object was indeed an instance. The method could easily have been used on a HttpRequest interface from the types package, and returned true.
1 parent b04c8d3 commit d9d24b0

File tree

15 files changed

+877
-64
lines changed

15 files changed

+877
-64
lines changed

packages/middleware-host-header/src/index.spec.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,16 @@ describe("hostHeaderMiddleware", () => {
2020
expect(mockNextHandler.mock.calls[0][0].request.headers.host).toBe("foo.amazonaws.com");
2121
});
2222

23-
2423
it("should include port in host header when set", async () => {
2524
expect.assertions(2);
2625
const middleware = hostHeaderMiddleware({ requestHandler: {} as any });
2726
const handler = middleware(mockNextHandler, {} as any);
2827
await handler({
2928
input: {},
30-
request: new HttpRequest({ hostname: "foo.amazonaws.com", port: 443 }),
29+
request: new HttpRequest({ hostname: "foo.amazonaws.com", port: 9000 }),
3130
});
3231
expect(mockNextHandler.mock.calls.length).toEqual(1);
33-
expect(mockNextHandler.mock.calls[0][0].request.headers.host).toBe("foo.amazonaws.com:443");
32+
expect(mockNextHandler.mock.calls[0][0].request.headers.host).toBe("foo.amazonaws.com:9000");
3433
});
3534

3635
it("should not set host header if already set", async () => {

packages/middleware-sdk-transcribe-streaming/src/middleware-endpoint.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("websocketURLMiddleware", () => {
4545
expect(HttpRequest.isInstance(args.request)).toBeTruthy();
4646
const processed = args.request as HttpRequest;
4747
expect(processed.protocol).toEqual("wss:");
48-
expect(processed.hostname).toEqual("transcribestreaming.us-east-1.amazonaws.com:8443");
48+
expect(processed.port).toEqual(8443);
4949
expect(processed.path).toEqual("/stream-transcription-websocket");
5050
expect(processed.method).toEqual("GET");
5151
done();

packages/middleware-sdk-transcribe-streaming/src/middleware-endpoint.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export const websocketURLMiddleware =
1919
if (HttpRequest.isInstance(request) && options.requestHandler.metadata?.handlerProtocol === "websocket") {
2020
// Update http/2 endpoint to WebSocket-specific endpoint.
2121
request.protocol = "wss:";
22-
// Append port to hostname because it needs to be signed together
23-
request.hostname = `${request.hostname}:8443`;
22+
// Update port for using WebSocket.
23+
request.port = 8443;
2424
request.path = `${request.path}-websocket`;
2525
request.method = "GET";
2626

packages/protocol-http/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build:types": "tsc -p tsconfig.types.json",
1010
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
1111
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
12-
"test": "jest"
12+
"test": "jest --coverage"
1313
},
1414
"main": "./dist-cjs/index.js",
1515
"module": "./dist-es/index.js",

packages/protocol-http/src/Field.ts

+13-15
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { FieldPosition } from "./FieldPosition";
22

3-
export type FieldOptions = {
3+
export type FieldOptions = {
44
name: string;
5-
kind?: FieldPosition
5+
kind?: FieldPosition;
66
values?: string[];
77
};
88

99
/**
1010
* A name-value pair representing a single field
1111
* transmitted in an HTTP Request or Response.
12-
*
12+
*
1313
* The kind will dictate metadata placement within
1414
* an HTTP message.
15-
*
15+
*
1616
* All field names are case insensitive and
1717
* case-variance must be treated as equivalent.
1818
* Names MAY be normalized but SHOULD be preserved
@@ -31,8 +31,8 @@ export class Field {
3131
}
3232

3333
/**
34-
* Appends a value to the field.
35-
*
34+
* Appends a value to the field.
35+
*
3636
* @param value The value to append.
3737
*/
3838
public add(value: string): void {
@@ -41,7 +41,7 @@ export class Field {
4141

4242
/**
4343
* Overwrite existing field values.
44-
*
44+
*
4545
* @param values The new field values.
4646
*/
4747
public set(values: string[]): void {
@@ -50,28 +50,26 @@ export class Field {
5050

5151
/**
5252
* Remove all matching entries from list.
53-
*
53+
*
5454
* @param value Value to remove.
5555
*/
5656
public remove(value: string): void {
5757
this.values = this.values.filter((v) => v !== value);
5858
}
5959

6060
/**
61-
* Get comma-delimited string.
62-
*
61+
* Get comma-delimited string to be sent over the wire.
62+
*
6363
* @returns String representation of {@link Field}.
6464
*/
6565
public toString(): string {
66-
// Values with spaces or commas MUST be double-quoted
67-
return this.values
68-
.map((v) => (v.includes(",") || v.includes(" ") ? `"${v}"` : v))
69-
.join(", ");
66+
// Values with commas MUST be double-quoted
67+
return this.values.map((v) => (v.includes(",") ? `"${v}"` : v)).join(", ");
7068
}
7169

7270
/**
7371
* Get string values as a list
74-
*
72+
*
7573
* @returns Values in {@link Field} as a list.
7674
*/
7775
public get(): string[] {

packages/protocol-http/src/Fields.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Field } from "./Field";
1+
import { Field, FieldOptions } from "./Field";
22
import { FieldPosition } from "./FieldPosition";
33

4-
export type FieldsOptions = { fields?: Field[]; encoding?: string; };
4+
export type FieldsOptions = { fields?: Field[]; encoding?: string };
55

66
/**
77
* Collection of Field entries mapped by name.
@@ -18,7 +18,7 @@ export class Fields {
1818
/**
1919
* Set entry for a {@link Field} name. The `name`
2020
* attribute will be used to key the collection.
21-
*
21+
*
2222
* @param field The {@link Field} to set.
2323
*/
2424
public setField(field: Field): void {
@@ -27,7 +27,7 @@ export class Fields {
2727

2828
/**
2929
* Retrieve {@link Field} entry by name.
30-
*
30+
*
3131
* @param name The name of the {@link Field} entry
3232
* to retrieve
3333
* @returns The {@link Field} if it exists.
@@ -38,22 +38,48 @@ export class Fields {
3838

3939
/**
4040
* Delete entry from collection.
41-
*
41+
*
4242
* @param name Name of the entry to delete.
43-
*/
43+
*/
4444
public removeField(name: string): void {
4545
delete this.entries[name.toLowerCase()];
4646
}
4747

4848
/**
4949
* Helper function for retrieving specific types of fields.
5050
* Used to grab all headers or all trailers.
51-
*
51+
*
5252
* @param kind {@link FieldPosition} of entries to retrieve.
5353
* @returns The {@link Field} entries with the specified
5454
* {@link FieldPosition}.
5555
*/
5656
public getByType(kind: FieldPosition): Field[] {
5757
return Object.values(this.entries).filter((field) => field.kind === kind);
5858
}
59+
60+
/**
61+
* Retrieves all the {@link Field}s in the collection.
62+
* Includes headers and trailers.
63+
*
64+
* @returns All fields in the collection.
65+
*/
66+
public getAll(): Field[] {
67+
return Object.values(this.entries);
68+
}
69+
70+
/**
71+
* Utility for creating {@link Fields} without having to
72+
* construct each {@link Field} individually.
73+
*
74+
* @param fieldsToCreate List of arguments used to create each
75+
* {@link Field}.
76+
* @param encoding Optional encoding of resultant {@link Fields}.
77+
* @returns The {@link Fields} instance.
78+
*/
79+
public static from(fieldsToCreate: FieldOptions[], encoding?: string): Fields {
80+
return fieldsToCreate.reduce((fields, fieldArgs) => {
81+
fields.setField(new Field(fieldArgs));
82+
return fields;
83+
}, new Fields({ encoding }));
84+
}
5985
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Fields } from "./Fields";
2+
import { getHeadersProxy, headersToFields, headerValueToFieldValues } from "./headersProxy";
3+
4+
describe("getHeadersProxy", () => {
5+
const mockHeaders = {
6+
foo: "bar",
7+
baz: "qux,quux",
8+
};
9+
const mockFields = Fields.from([
10+
{ name: "foo", values: ["bar"] },
11+
{ name: "baz", values: ["qux", "quux"] },
12+
]);
13+
14+
describe("proxy works like a normal Record", () => {
15+
describe("access and mutation", () => {
16+
it("can get and set individual keys", () => {
17+
const headers = getHeadersProxy(new Fields({}));
18+
headers["foo"] = "bar";
19+
headers.baz = "qux,quux";
20+
expect(headers["foo"]).toEqual("bar");
21+
expect(headers.baz).toEqual("qux,quux");
22+
});
23+
24+
it("can be updated using object spread syntax", () => {
25+
let headers = getHeadersProxy(mockFields);
26+
headers = { ...headers, another: "value" };
27+
expect(headers).toEqual({ ...mockHeaders, another: "value" });
28+
});
29+
30+
it("can delete keys", () => {
31+
const headers = getHeadersProxy(new Fields({ fields: mockFields.getAll() }));
32+
delete headers["foo"];
33+
delete headers.baz;
34+
expect(headers).toEqual({});
35+
});
36+
});
37+
38+
describe("iteration", () => {
39+
it("can be iterated over using Object.keys", () => {
40+
const headers = getHeadersProxy(mockFields);
41+
const keys = Object.keys(headers);
42+
expect(keys).toEqual(["foo", "baz"]);
43+
});
44+
45+
it("can be iterated over using Object.values", () => {
46+
const headers = getHeadersProxy(mockFields);
47+
const values = Object.values(headers);
48+
expect(values).toEqual(["bar", "qux,quux"]);
49+
});
50+
51+
it("can be iterated over using Object.entries", () => {
52+
const headers = getHeadersProxy(mockFields);
53+
const entries = Object.entries(headers);
54+
expect(entries).toEqual([
55+
["foo", "bar"],
56+
["baz", "qux,quux"],
57+
]);
58+
});
59+
60+
it("can be iterated over using `for..in`", () => {
61+
const keys: string[] = [];
62+
const headers = getHeadersProxy(mockFields);
63+
for (const key in headers) {
64+
keys.push(key);
65+
}
66+
expect(keys).toEqual(["foo", "baz"]);
67+
});
68+
});
69+
});
70+
71+
describe("proxies the fields", () => {
72+
it("updates fields when individual keys are set on headers", () => {
73+
const fields = new Fields({});
74+
const headers = getHeadersProxy(fields);
75+
headers["foo"] = "bar";
76+
headers.baz = "qux,quux";
77+
expect(fields).toEqual(mockFields);
78+
});
79+
80+
it("updates fields when keys are deleted from headers", () => {
81+
const fields = new Fields({ fields: mockFields.getAll() });
82+
const headers = getHeadersProxy(fields);
83+
delete headers["foo"];
84+
delete headers.baz;
85+
expect(fields).toEqual(new Fields({}));
86+
});
87+
88+
it("can get values from fields or headers", () => {
89+
const headers = getHeadersProxy(mockFields);
90+
expect(headers["foo"]).toEqual(mockFields.getField("foo")?.values.join(","));
91+
expect(headers.baz).toEqual(mockFields.getField("baz")?.values.join(","));
92+
});
93+
94+
it("does not proxy class properties of fields", () => {
95+
const fields = new Fields({});
96+
Object.defineProperty(fields, "foo", {
97+
value: "bar",
98+
enumerable: true,
99+
writable: true,
100+
configurable: true,
101+
});
102+
const headers = getHeadersProxy(fields);
103+
Object.keys(fields).forEach((key) => {
104+
expect(headers[key]).toBe(undefined);
105+
});
106+
});
107+
108+
it("can use Object prototype methods", () => {
109+
const fields = new Fields({ fields: mockFields.getAll() });
110+
const headers = getHeadersProxy(fields);
111+
delete headers["foo"];
112+
Object.defineProperty(headers, "fizz", {
113+
value: "buzz",
114+
enumerable: true,
115+
writable: true,
116+
configurable: true,
117+
});
118+
expect(headers.hasOwnProperty("foo")).toBe(false);
119+
expect(headers.hasOwnProperty("baz")).toBe(true);
120+
expect(headers.hasOwnProperty("fizz")).toBe(true);
121+
expect(headers["fizz"]).toEqual("buzz");
122+
expect("fizz" in headers).toBe(true);
123+
expect(headers.hasOwnProperty("encoding")).toBe(false);
124+
expect({ ...headers }).toEqual({ fizz: "buzz", baz: "qux,quux" });
125+
expect(fields.getField("foo")).not.toBeDefined();
126+
expect(fields.getField("fizz")?.toString()).toEqual("buzz");
127+
});
128+
});
129+
});
130+
131+
describe("headersToFields", () => {
132+
it("ignores null and undefined values", () => {
133+
const headers = { foo: null as any, bar: undefined as any };
134+
const fields = headersToFields(headers);
135+
expect(fields.getField("foo")).not.toBeDefined();
136+
});
137+
});
138+
139+
describe("headerValueToFieldValues", () => {
140+
it("ignores null and undefined values", () => {
141+
expect(headerValueToFieldValues(undefined as any)).not.toBeDefined();
142+
expect(headerValueToFieldValues(null as any)).not.toBeDefined();
143+
});
144+
it("parses single string value", () => {
145+
const headerValue = "foo";
146+
expect(headerValueToFieldValues(headerValue)).toEqual(["foo"]);
147+
});
148+
it("parses comma-separated string value", () => {
149+
const headerValue = "foo,bar";
150+
expect(headerValueToFieldValues(headerValue)).toEqual(["foo", "bar"]);
151+
});
152+
it("preserves whitespace", () => {
153+
const headerValue = "foo, bar ";
154+
expect(headerValueToFieldValues(headerValue)).toEqual(["foo", " bar "]);
155+
});
156+
});

0 commit comments

Comments
 (0)