Skip to content

feat(protocol-http): implement SRA HttpRequest #4514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 13, 2023
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
5 changes: 2 additions & 3 deletions packages/middleware-host-header/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,16 @@ describe("hostHeaderMiddleware", () => {
expect(mockNextHandler.mock.calls[0][0].request.headers.host).toBe("foo.amazonaws.com");
});


it("should include port in host header when set", async () => {
expect.assertions(2);
const middleware = hostHeaderMiddleware({ requestHandler: {} as any });
const handler = middleware(mockNextHandler, {} as any);
await handler({
input: {},
request: new HttpRequest({ hostname: "foo.amazonaws.com", port: 443 }),
request: new HttpRequest({ hostname: "foo.amazonaws.com", port: 9000 }),
});
expect(mockNextHandler.mock.calls.length).toEqual(1);
expect(mockNextHandler.mock.calls[0][0].request.headers.host).toBe("foo.amazonaws.com:443");
expect(mockNextHandler.mock.calls[0][0].request.headers.host).toBe("foo.amazonaws.com:9000");
});

it("should not set host header if already set", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("websocketURLMiddleware", () => {
expect(HttpRequest.isInstance(args.request)).toBeTruthy();
const processed = args.request as HttpRequest;
expect(processed.protocol).toEqual("wss:");
expect(processed.hostname).toEqual("transcribestreaming.us-east-1.amazonaws.com:8443");
expect(processed.port).toEqual(8443);
expect(processed.path).toEqual("/stream-transcription-websocket");
expect(processed.method).toEqual("GET");
done();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const websocketURLMiddleware =
if (HttpRequest.isInstance(request) && options.requestHandler.metadata?.handlerProtocol === "websocket") {
// Update http/2 endpoint to WebSocket-specific endpoint.
request.protocol = "wss:";
// Append port to hostname because it needs to be signed together
request.hostname = `${request.hostname}:8443`;
// Update port for using WebSocket.
request.port = 8443;
request.path = `${request.path}-websocket`;
request.method = "GET";

Expand Down
2 changes: 1 addition & 1 deletion packages/protocol-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build:types": "tsc -p tsconfig.types.json",
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
"test": "jest"
"test": "jest --coverage"
},
"main": "./dist-cjs/index.js",
"module": "./dist-es/index.js",
Expand Down
28 changes: 13 additions & 15 deletions packages/protocol-http/src/Field.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { FieldPosition } from "./FieldPosition";

export type FieldOptions = {
export type FieldOptions = {
name: string;
kind?: FieldPosition
kind?: FieldPosition;
values?: string[];
};

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

/**
* Appends a value to the field.
*
* Appends a value to the field.
*
* @param value The value to append.
*/
public add(value: string): void {
Expand All @@ -41,7 +41,7 @@ export class Field {

/**
* Overwrite existing field values.
*
*
* @param values The new field values.
*/
public set(values: string[]): void {
Expand All @@ -50,28 +50,26 @@ export class Field {

/**
* Remove all matching entries from list.
*
*
* @param value Value to remove.
*/
public remove(value: string): void {
this.values = this.values.filter((v) => v !== value);
}

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

/**
* Get string values as a list
*
*
* @returns Values in {@link Field} as a list.
*/
public get(): string[] {
Expand Down
40 changes: 33 additions & 7 deletions packages/protocol-http/src/Fields.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Field } from "./Field";
import { Field, FieldOptions } from "./Field";
import { FieldPosition } from "./FieldPosition";

export type FieldsOptions = { fields?: Field[]; encoding?: string; };
export type FieldsOptions = { fields?: Field[]; encoding?: string };

/**
* Collection of Field entries mapped by name.
Expand All @@ -18,7 +18,7 @@ export class Fields {
/**
* Set entry for a {@link Field} name. The `name`
* attribute will be used to key the collection.
*
*
* @param field The {@link Field} to set.
*/
public setField(field: Field): void {
Expand All @@ -27,7 +27,7 @@ export class Fields {

/**
* Retrieve {@link Field} entry by name.
*
*
* @param name The name of the {@link Field} entry
* to retrieve
* @returns The {@link Field} if it exists.
Expand All @@ -38,22 +38,48 @@ export class Fields {

/**
* Delete entry from collection.
*
*
* @param name Name of the entry to delete.
*/
*/
public removeField(name: string): void {
delete this.entries[name.toLowerCase()];
}

/**
* Helper function for retrieving specific types of fields.
* Used to grab all headers or all trailers.
*
*
* @param kind {@link FieldPosition} of entries to retrieve.
* @returns The {@link Field} entries with the specified
* {@link FieldPosition}.
*/
public getByType(kind: FieldPosition): Field[] {
return Object.values(this.entries).filter((field) => field.kind === kind);
}

/**
* Retrieves all the {@link Field}s in the collection.
* Includes headers and trailers.
*
* @returns All fields in the collection.
*/
public getAll(): Field[] {
return Object.values(this.entries);
}

/**
* Utility for creating {@link Fields} without having to
* construct each {@link Field} individually.
*
* @param fieldsToCreate List of arguments used to create each
* {@link Field}.
* @param encoding Optional encoding of resultant {@link Fields}.
* @returns The {@link Fields} instance.
*/
public static from(fieldsToCreate: FieldOptions[], encoding?: string): Fields {
return fieldsToCreate.reduce((fields, fieldArgs) => {
fields.setField(new Field(fieldArgs));
return fields;
}, new Fields({ encoding }));
}
}
156 changes: 156 additions & 0 deletions packages/protocol-http/src/headersProxy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Fields } from "./Fields";
import { getHeadersProxy, headersToFields, headerValueToFieldValues } from "./headersProxy";

describe("getHeadersProxy", () => {
const mockHeaders = {
foo: "bar",
baz: "qux,quux",
};
const mockFields = Fields.from([
{ name: "foo", values: ["bar"] },
{ name: "baz", values: ["qux", "quux"] },
]);

describe("proxy works like a normal Record", () => {
describe("access and mutation", () => {
it("can get and set individual keys", () => {
const headers = getHeadersProxy(new Fields({}));
headers["foo"] = "bar";
headers.baz = "qux,quux";
expect(headers["foo"]).toEqual("bar");
expect(headers.baz).toEqual("qux,quux");
});

it("can be updated using object spread syntax", () => {
let headers = getHeadersProxy(mockFields);
headers = { ...headers, another: "value" };
expect(headers).toEqual({ ...mockHeaders, another: "value" });
});

it("can delete keys", () => {
const headers = getHeadersProxy(new Fields({ fields: mockFields.getAll() }));
delete headers["foo"];
delete headers.baz;
expect(headers).toEqual({});
});
});

describe("iteration", () => {
it("can be iterated over using Object.keys", () => {
const headers = getHeadersProxy(mockFields);
const keys = Object.keys(headers);
expect(keys).toEqual(["foo", "baz"]);
});

it("can be iterated over using Object.values", () => {
const headers = getHeadersProxy(mockFields);
const values = Object.values(headers);
expect(values).toEqual(["bar", "qux,quux"]);
});

it("can be iterated over using Object.entries", () => {
const headers = getHeadersProxy(mockFields);
const entries = Object.entries(headers);
expect(entries).toEqual([
["foo", "bar"],
["baz", "qux,quux"],
]);
});

it("can be iterated over using `for..in`", () => {
const keys: string[] = [];
const headers = getHeadersProxy(mockFields);
for (const key in headers) {
keys.push(key);
}
expect(keys).toEqual(["foo", "baz"]);
});
});
});

describe("proxies the fields", () => {
it("updates fields when individual keys are set on headers", () => {
const fields = new Fields({});
const headers = getHeadersProxy(fields);
headers["foo"] = "bar";
headers.baz = "qux,quux";
expect(fields).toEqual(mockFields);
});

it("updates fields when keys are deleted from headers", () => {
const fields = new Fields({ fields: mockFields.getAll() });
const headers = getHeadersProxy(fields);
delete headers["foo"];
delete headers.baz;
expect(fields).toEqual(new Fields({}));
});

it("can get values from fields or headers", () => {
const headers = getHeadersProxy(mockFields);
expect(headers["foo"]).toEqual(mockFields.getField("foo")?.values.join(","));
expect(headers.baz).toEqual(mockFields.getField("baz")?.values.join(","));
});

it("does not proxy class properties of fields", () => {
const fields = new Fields({});
Object.defineProperty(fields, "foo", {
value: "bar",
enumerable: true,
writable: true,
configurable: true,
});
const headers = getHeadersProxy(fields);
Object.keys(fields).forEach((key) => {
expect(headers[key]).toBe(undefined);
});
});

it("can use Object prototype methods", () => {
const fields = new Fields({ fields: mockFields.getAll() });
const headers = getHeadersProxy(fields);
delete headers["foo"];
Object.defineProperty(headers, "fizz", {
value: "buzz",
enumerable: true,
writable: true,
configurable: true,
});
expect(headers.hasOwnProperty("foo")).toBe(false);
expect(headers.hasOwnProperty("baz")).toBe(true);
expect(headers.hasOwnProperty("fizz")).toBe(true);
expect(headers["fizz"]).toEqual("buzz");
expect("fizz" in headers).toBe(true);
expect(headers.hasOwnProperty("encoding")).toBe(false);
expect({ ...headers }).toEqual({ fizz: "buzz", baz: "qux,quux" });
expect(fields.getField("foo")).not.toBeDefined();
expect(fields.getField("fizz")?.toString()).toEqual("buzz");
});
});
});

describe("headersToFields", () => {
it("ignores null and undefined values", () => {
const headers = { foo: null as any, bar: undefined as any };
const fields = headersToFields(headers);
expect(fields.getField("foo")).not.toBeDefined();
});
});

describe("headerValueToFieldValues", () => {
it("ignores null and undefined values", () => {
expect(headerValueToFieldValues(undefined as any)).not.toBeDefined();
expect(headerValueToFieldValues(null as any)).not.toBeDefined();
});
it("parses single string value", () => {
const headerValue = "foo";
expect(headerValueToFieldValues(headerValue)).toEqual(["foo"]);
});
it("parses comma-separated string value", () => {
const headerValue = "foo,bar";
expect(headerValueToFieldValues(headerValue)).toEqual(["foo", "bar"]);
});
it("preserves whitespace", () => {
const headerValue = "foo, bar ";
expect(headerValueToFieldValues(headerValue)).toEqual(["foo", " bar "]);
});
});
Loading