import type { HttpHandlerOptions } from "@effect-aws/commons";
import * as HttpHandler from "@effect-aws/commons/HttpHandler";
import type { HttpRequest } from "@smithy/protocol-http";
import { HttpResponse } from "@smithy/protocol-http";
import { buildQueryString } from "@smithy/querystring-builder";
import type { RequestHandlerOutput } from "@smithy/types";
import type * as Cause from "effect/Cause";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import type { HttpMethod } from "effect/http";
import { HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/http";
import * as Option from "effect/Option";
import type * as Scope from "effect/Scope";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
declare module "effect/http" {
interface HttpClientResponse {
/**
* @private
*/
source: globalThis.Response;
/**
* @private
* @since 0.79.0
*/
original?: HttpClientResponse;
}
}
const makeUrl = (request: HttpRequest): string => {
let path = request.path;
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}@`;
}
return `${request.protocol}//${auth}${request.hostname}${request.port ? `:${request.port}` : ""}${path}`;
};
const tryToReadableStream = (stream: Stream.Stream) =>
Stream.peel(stream, Sink.head()).pipe(
Effect.map(([head, tailStream]) =>
Option.isNone(head) ? Stream.empty : Stream.merge(Stream.make(head.value), tailStream)
),
Effect.flatMap(Stream.toReadableStreamEffect()),
);
/** @internal */
export const makeHttpClientRequestHandler = (config: HttpHandlerOptions) =>
Effect.gen(function*() {
const client = yield* HttpClient.HttpClient;
return HttpHandler.make({
handle: (
request: HttpRequest,
handlerOptions?: HttpHandlerOptions,
): Effect.Effect<
RequestHandlerOutput,
Cause.TimeoutError,
Scope.Scope
> =>
Effect.gen(function*() {
const requestTimeoutInMs = Option.fromNullishOr(handlerOptions?.requestTimeout ?? config.requestTimeout).pipe(
Option.map(Duration.millis),
Option.getOrElse(() => Duration.infinity),
);
const url = makeUrl(request);
// Request constructor doesn't allow GET/HEAD request with body
// ref: https://github.com/whatwg/fetch/issues/551
const body = request.method === "GET" || request.method === "HEAD"
? HttpBody.empty
: HttpBody.raw(request.body);
const fetch = HttpClientRequest.make(request.method as HttpMethod.HttpMethod);
const httpRequest = fetch(url, { body, headers: request.headers });
const response = yield* client.execute(httpRequest).pipe(
Effect.flatMap((res) =>
tryToReadableStream(res.stream).pipe(
Effect.catchTag(
"HttpClientError",
(error) =>
error.reason instanceof HttpClientError.EmptyBodyError ? res.arrayBuffer : Effect.fail(error),
),
Effect.map((body) =>
new HttpResponse({
headers: res.headers,
// reason: (res.original ?? res).source.statusText, // changed since @effect/platform@0.79.0
statusCode: res.status,
body,
})
),
)
),
Effect.orDie,
Effect.timeout(requestTimeoutInMs),
);
return { response };
}),
});
});