Skip to content

Commit

Permalink
feat(protocol-http): add uri type and implementation (#4771)
Browse files Browse the repository at this point in the history
Adds the URI type to types package, and uses it in HttpRequest.
The URI type adds additional properties that didn't exist on Endpoint.
Http handlers were updated to use these new properties when constructing
request URLs. Tests were also added to make sure handlers construct URLs
properly with the new properties.
  • Loading branch information
milesziemer authored Jun 6, 2023
1 parent d29cb58 commit 5c7b40e
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 17 deletions.
8 changes: 6 additions & 2 deletions packages/fetch-http-handler/src/fetch-http-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ describe(FetchHttpHandler.name, () => {
headers: {},
hostname: "foo.amazonaws.com",
method: "GET",
path: "/test/?bar=baz",
path: "/test",
query: { bar: "baz" },
username: "username",
password: "password",
fragment: "fragment",
protocol: "https:",
port: 443,
});
Expand All @@ -105,7 +109,7 @@ describe(FetchHttpHandler.name, () => {

expect(mockFetch.mock.calls.length).toBe(1);
const requestCall = mockRequest.mock.calls[0];
expect(requestCall[0]).toBe("https://foo.amazonaws.com:443/test/?bar=baz");
expect(requestCall[0]).toBe("https://username:password@foo.amazonaws.com:443/test?bar=baz#fragment");
});

it("will omit body if method is GET", async () => {
Expand Down
20 changes: 14 additions & 6 deletions packages/fetch-http-handler/src/fetch-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,23 @@ export class FetchHttpHandler implements HttpHandler {
}

let path = request.path;
if (request.query) {
const queryString = buildQueryString(request.query);
if (queryString) {
path += `?${queryString}`;
}
const queryString = buildQueryString(request.query || {});
if (queryString) {
path += `?${queryString}`;
}
if (request.fragment) {
path += `#${request.fragment}`;
}

let auth = "";
if (request.username != null || request.password != null) {
const username = request.username ?? "";
const password = request.password ?? "";
auth = `${username}:${password}@`;
}

const { port, method } = request;
const url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ""}${path}`;
const url = `${request.protocol}//${auth}${request.hostname}${port ? `:${port}` : ""}${path}`;
// Request constructor doesn't allow GET/HEAD request with body
// ref: https://github.com/whatwg/fetch/issues/551
const body = method === "GET" || method === "HEAD" ? undefined : request.body;
Expand Down
21 changes: 21 additions & 0 deletions packages/node-http-handler/src/node-http-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,27 @@ describe("NodeHttpHandler", () => {
expect(providerInvokedCount).toBe(1);
expect(providerResolvedCount).toBe(1);
});

it("sends requests to the right url", async () => {
const nodeHttpHandler = new NodeHttpHandler({});
const httpRequest = {
protocol: "http:",
username: "username",
password: "password",
hostname: "host",
port: 1234,
path: "/some/path",
query: {
some: "query",
},
fragment: "fragment",
};
await nodeHttpHandler.handle(httpRequest as any);
expect(hRequestSpy.mock.calls[0][0]?.auth).toEqual("username:password");
expect(hRequestSpy.mock.calls[0][0]?.host).toEqual("host");
expect(hRequestSpy.mock.calls[0][0]?.port).toEqual(1234);
expect(hRequestSpy.mock.calls[0][0]?.path).toEqual("/some/path?some=query#fragment");
});
});
});

Expand Down
16 changes: 15 additions & 1 deletion packages/node-http-handler/src/node-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,27 @@ export class NodeHttpHandler implements HttpHandler {
// determine which http(s) client to use
const isSSL = request.protocol === "https:";
const queryString = buildQueryString(request.query || {});
let auth = undefined;
if (request.username != null || request.password != null) {
const username = request.username ?? "";
const password = request.password ?? "";
auth = `${username}:${password}`;
}
let path = request.path;
if (queryString) {
path += `?${queryString}`;
}
if (request.fragment) {
path += `#${request.fragment}`;
}
const nodeHttpsOptions: RequestOptions = {
headers: request.headers,
host: request.hostname,
method: request.method,
path: queryString ? `${request.path}?${queryString}` : request.path,
path,
port: request.port,
agent: isSSL ? this.config.httpsAgent : this.config.httpAgent,
auth,
};

// create the http request
Expand Down
18 changes: 18 additions & 0 deletions packages/node-http-handler/src/node-http2-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,4 +625,22 @@ describe(NodeHttp2Handler.name, () => {
});
});
});

it("sends the request to the correct url", async () => {
const server = createMockHttp2Server();
server.on("request", (request, response) => {
expect(request.url).toBe("http://foo:bar@localhost/foo/bar?foo=bar#foo");
response.statusCode = 200;
});
const handler = new NodeHttp2Handler({});
await handler.handle({
...getMockReqOptions(),
username: "foo",
password: "bar",
path: "/foo/bar",
query: { foo: "bar" },
fragment: "foo",
} as any);
handler.destroy();
});
});
19 changes: 16 additions & 3 deletions packages/node-http-handler/src/node-http2-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,14 @@ export class NodeHttp2Handler implements HttpHandler {
return;
}

const { hostname, method, port, protocol, path, query } = request;
const authority = `${protocol}//${hostname}${port ? `:${port}` : ""}`;
const { hostname, method, port, protocol, query } = request;
let auth = "";
if (request.username != null || request.password != null) {
const username = request.username ?? "";
const password = request.password ?? "";
auth = `${username}:${password}@`;
}
const authority = `${protocol}//${auth}${hostname}${port ? `:${port}` : ""}`;
const requestContext = { destination: new URL(authority) } as RequestContext;
const session = this.connectionManager.lease(requestContext, {
requestTimeout: this.config?.sessionTimeout,
Expand All @@ -117,10 +123,17 @@ export class NodeHttp2Handler implements HttpHandler {
};

const queryString = buildQueryString(query || {});
let path = request.path;
if (queryString) {
path += `?${queryString}`;
}
if (request.fragment) {
path += `#${request.fragment}`;
}
// create the http2 request
const req = session.request({
...request.headers,
[constants.HTTP2_HEADER_PATH]: queryString ? `${path}?${queryString}` : path,
[constants.HTTP2_HEADER_PATH]: path,
[constants.HTTP2_HEADER_METHOD]: method,
});

Expand Down
12 changes: 9 additions & 3 deletions packages/protocol-http/src/httpRequest.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { Endpoint, HeaderBag, HttpMessage, HttpRequest as IHttpRequest, QueryParameterBag } from "@aws-sdk/types";
import { HeaderBag, HttpMessage, HttpRequest as IHttpRequest, QueryParameterBag, URI } from "@aws-sdk/types";

type HttpRequestOptions = Partial<HttpMessage> & Partial<Endpoint> & { method?: string };
type HttpRequestOptions = Partial<HttpMessage> & Partial<URI> & { method?: string };

export interface HttpRequest extends IHttpRequest {}

export class HttpRequest implements HttpMessage, Endpoint {
export class HttpRequest implements HttpMessage, URI {
public method: string;
public protocol: string;
public hostname: string;
public port?: number;
public path: string;
public query: QueryParameterBag;
public headers: HeaderBag;
public username?: string;
public password?: string;
public fragment?: string;
public body?: any;

constructor(options: HttpRequestOptions) {
Expand All @@ -27,6 +30,9 @@ export class HttpRequest implements HttpMessage, Endpoint {
: options.protocol
: "https:";
this.path = options.path ? (options.path.charAt(0) !== "/" ? `/${options.path}` : options.path) : "/";
this.username = options.username;
this.password = options.password;
this.fragment = options.fragment;
}

static isInstance(request: unknown): request is HttpRequest {
Expand Down
4 changes: 3 additions & 1 deletion packages/types/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AbortSignal } from "./abort";
import { URI } from "./uri";

/**
* @public
*
Expand Down Expand Up @@ -86,7 +88,7 @@ export interface Endpoint {
* Interface an HTTP request class. Contains
* addressing information in addition to standard message properties.
*/
export interface HttpRequest extends HttpMessage, Endpoint {
export interface HttpRequest extends HttpMessage, URI {
method: string;
}

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export * from "./signature";
export * from "./stream";
export * from "./token";
export * from "./transfer";
export * from "./uri";
export * from "./util";
export * from "./waiter";
18 changes: 18 additions & 0 deletions packages/types/src/uri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { QueryParameterBag } from "./http";

/**
* @internal
*
* Represents the components parts of a Uniform Resource Identifier used to
* construct the target location of a Request.
*/
export type URI = {
protocol: string;
hostname: string;
port?: number;
path: string;
query?: QueryParameterBag;
username?: string;
password?: string;
fragment?: string;
};
12 changes: 11 additions & 1 deletion packages/util-format-url/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,15 @@ export function formatUrl(request: Omit<HttpRequest, "headers" | "method">): str
if (queryString && queryString[0] !== "?") {
queryString = `?${queryString}`;
}
return `${protocol}//${hostname}${path}${queryString}`;
let auth = "";
if (request.username != null || request.password != null) {
const username = request.username ?? "";
const password = request.password ?? "";
auth = `${username}:${password}@`;
}
let fragment = "";
if (request.fragment) {
fragment = `#${request.fragment}`;
}
return `${protocol}//${auth}${hostname}${path}${queryString}${fragment}`;
}

0 comments on commit 5c7b40e

Please sign in to comment.