unstructured-client
Version:
<h3 align="center"> <img src="https://raw.githubusercontent.com/Unstructured-IO/unstructured/main/img/unstructured_logo.png" height="200" > </h3>
404 lines (361 loc) • 11.5 kB
text/typescript
/*
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
*/
import { SDKHooks } from "../hooks/hooks.js";
import { HookContext } from "../hooks/types.js";
import {
ConnectionError,
InvalidRequestError,
RequestAbortedError,
RequestTimeoutError,
UnexpectedClientError,
} from "../sdk/models/errors/httpclienterrors.js";
import { ERR, OK, Result } from "../sdk/types/fp.js";
import { stringToBase64 } from "./base64.js";
import { SDK_METADATA, SDKOptions, serverURLFromOptions } from "./config.js";
import { encodeForm } from "./encodings.js";
import {
HTTPClient,
isAbortError,
isConnectionError,
isTimeoutError,
matchContentType,
matchStatusCode,
} from "./http.js";
import { Logger } from "./logger.js";
import { retry, RetryConfig } from "./retries.js";
import { SecurityState } from "./security.js";
export type RequestOptions = {
/**
* Sets a timeout, in milliseconds, on HTTP requests made by an SDK method. If
* `fetchOptions.signal` is set then it will take precedence over this option.
*/
timeoutMs?: number;
/**
* Set or override a retry policy on HTTP calls.
*/
retries?: RetryConfig;
/**
* Specifies the status codes which should be retried using the given retry policy.
*/
retryCodes?: string[];
/**
* Overrides the base server URL that will be used by an operation.
*/
serverURL?: string | URL;
/**
* @deprecated `fetchOptions` has been flattened into `RequestOptions`.
*
* Sets various request options on the `fetch` call made by an SDK method.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#options|Request}
*/
fetchOptions?: Omit<RequestInit, "method" | "body">;
} & Omit<RequestInit, "method" | "body">;
type RequestConfig = {
method: string;
path: string;
baseURL?: string | URL | undefined;
query?: string;
body?: RequestInit["body"];
headers?: HeadersInit;
security?: SecurityState | null;
uaHeader?: string;
userAgent?: string | undefined;
timeoutMs?: number;
};
const gt: unknown = typeof globalThis === "undefined" ? null : globalThis;
const webWorkerLike = typeof gt === "object"
&& gt != null
&& "importScripts" in gt
&& typeof gt["importScripts"] === "function";
const isBrowserLike = webWorkerLike
|| (typeof navigator !== "undefined" && "serviceWorker" in navigator)
|| (typeof window === "object" && typeof window.document !== "undefined");
export class ClientSDK {
readonly #httpClient: HTTPClient;
readonly #hooks: SDKHooks;
readonly #logger?: Logger | undefined;
public readonly _baseURL: URL | null;
public readonly _options: SDKOptions & { hooks?: SDKHooks };
constructor(options: SDKOptions = {}) {
const opt = options as unknown;
if (
typeof opt === "object"
&& opt != null
&& "hooks" in opt
&& opt.hooks instanceof SDKHooks
) {
this.#hooks = opt.hooks;
} else {
this.#hooks = new SDKHooks();
}
const url = serverURLFromOptions(options);
if (url) {
url.pathname = url.pathname.replace(/\/+$/, "") + "/";
}
const { baseURL, client } = this.#hooks.sdkInit({
baseURL: url,
client: options.httpClient || new HTTPClient(),
});
this._baseURL = baseURL;
this.#httpClient = client;
this._options = { ...options, hooks: this.#hooks };
this.#logger = this._options.debugLogger;
}
public _createRequest(
context: HookContext,
conf: RequestConfig,
options?: RequestOptions,
): Result<Request, InvalidRequestError | UnexpectedClientError> {
const { method, path, query, headers: opHeaders, security } = conf;
const base = conf.baseURL ?? this._baseURL;
if (!base) {
return ERR(new InvalidRequestError("No base URL provided for operation"));
}
const reqURL = new URL(base);
const inputURL = new URL(path, reqURL);
if (path) {
reqURL.pathname += reqURL.pathname.endsWith("/") ? "" : "/";
reqURL.pathname += inputURL.pathname.replace(/^\/+/, "");
}
let finalQuery = query || "";
const secQuery: string[] = [];
for (const [k, v] of Object.entries(security?.queryParams || {})) {
const q = encodeForm(k, v, { charEncoding: "percent" });
if (typeof q !== "undefined") {
secQuery.push(q);
}
}
if (secQuery.length) {
finalQuery += `&${secQuery.join("&")}`;
}
if (finalQuery) {
const q = finalQuery.startsWith("&") ? finalQuery.slice(1) : finalQuery;
reqURL.search = `?${q}`;
}
const headers = new Headers(opHeaders);
const username = security?.basic.username;
const password = security?.basic.password;
if (username != null || password != null) {
const encoded = stringToBase64(
[username || "", password || ""].join(":"),
);
headers.set("Authorization", `Basic ${encoded}`);
}
const securityHeaders = new Headers(security?.headers || {});
for (const [k, v] of securityHeaders) {
headers.set(k, v);
}
let cookie = headers.get("cookie") || "";
for (const [k, v] of Object.entries(security?.cookies || {})) {
cookie += `; ${k}=${v}`;
}
cookie = cookie.startsWith("; ") ? cookie.slice(2) : cookie;
headers.set("cookie", cookie);
const userHeaders = new Headers(
options?.headers ?? options?.fetchOptions?.headers,
);
for (const [k, v] of userHeaders) {
headers.set(k, v);
}
// Only set user agent header in non-browser-like environments since CORS
// policy disallows setting it in browsers e.g. Chrome throws an error.
if (!isBrowserLike) {
headers.set(
conf.uaHeader ?? "user-agent",
conf.userAgent ?? SDK_METADATA.userAgent,
);
}
const fetchOptions: Omit<RequestInit, "method" | "body"> = {
...options?.fetchOptions,
...options,
};
if (!fetchOptions?.signal && conf.timeoutMs && conf.timeoutMs > 0) {
const timeoutSignal = AbortSignal.timeout(conf.timeoutMs);
fetchOptions.signal = timeoutSignal;
}
if (conf.body instanceof ReadableStream) {
Object.assign(fetchOptions, { duplex: "half" });
}
let input;
try {
input = this.#hooks.beforeCreateRequest(context, {
url: reqURL,
options: {
...fetchOptions,
body: conf.body ?? null,
headers,
method,
},
});
} catch (err: unknown) {
return ERR(
new UnexpectedClientError("Create request hook failed to execute", {
cause: err,
}),
);
}
return OK(new Request(input.url, input.options));
}
public async _do(
request: Request,
options: {
context: HookContext;
errorCodes: number | string | (number | string)[];
retryConfig: RetryConfig;
retryCodes: string[];
},
): Promise<
Result<
Response,
| RequestAbortedError
| RequestTimeoutError
| ConnectionError
| UnexpectedClientError
>
> {
const { context, errorCodes } = options;
return retry(
async () => {
const req = await this.#hooks.beforeRequest(context, request.clone());
await logRequest(this.#logger, req).catch((e) =>
this.#logger?.log("Failed to log request:", e)
);
let response = await this.#httpClient.request(req);
try {
if (matchStatusCode(response, errorCodes)) {
const result = await this.#hooks.afterError(
context,
response,
null,
);
if (result.error) {
throw result.error;
}
response = result.response || response;
} else {
response = await this.#hooks.afterSuccess(context, response);
}
} finally {
await logResponse(this.#logger, response, req)
.catch(e => this.#logger?.log("Failed to log response:", e));
}
return response;
},
{ config: options.retryConfig, statusCodes: options.retryCodes },
).then(
(r) => OK(r),
(err) => {
switch (true) {
case isAbortError(err):
return ERR(
new RequestAbortedError("Request aborted by client", {
cause: err,
}),
);
case isTimeoutError(err):
return ERR(
new RequestTimeoutError("Request timed out", { cause: err }),
);
case isConnectionError(err):
return ERR(
new ConnectionError("Unable to make request", { cause: err }),
);
default:
return ERR(
new UnexpectedClientError("Unexpected HTTP client error", {
cause: err,
}),
);
}
},
);
}
}
const jsonLikeContentTypeRE = /(application|text)\/.*?\+*json.*/;
const jsonlLikeContentTypeRE =
/(application|text)\/(.*?\+*\bjsonl\b.*|.*?\+*\bx-ndjson\b.*)/;
async function logRequest(logger: Logger | undefined, req: Request) {
if (!logger) {
return;
}
const contentType = req.headers.get("content-type");
const ct = contentType?.split(";")[0] || "";
logger.group(`> Request: ${req.method} ${req.url}`);
logger.group("Headers:");
for (const [k, v] of req.headers.entries()) {
logger.log(`${k}: ${v}`);
}
logger.groupEnd();
logger.group("Body:");
switch (true) {
case jsonLikeContentTypeRE.test(ct):
logger.log(await req.clone().json());
break;
case ct.startsWith("text/"):
logger.log(await req.clone().text());
break;
case ct === "multipart/form-data": {
const body = await req.clone().formData();
for (const [k, v] of body) {
const vlabel = v instanceof Blob ? "<Blob>" : v;
logger.log(`${k}: ${vlabel}`);
}
break;
}
default:
logger.log(`<${contentType}>`);
break;
}
logger.groupEnd();
logger.groupEnd();
}
async function logResponse(
logger: Logger | undefined,
res: Response,
req: Request,
) {
if (!logger) {
return;
}
const contentType = res.headers.get("content-type");
const ct = contentType?.split(";")[0] || "";
logger.group(`< Response: ${req.method} ${req.url}`);
logger.log("Status Code:", res.status, res.statusText);
logger.group("Headers:");
for (const [k, v] of res.headers.entries()) {
logger.log(`${k}: ${v}`);
}
logger.groupEnd();
logger.group("Body:");
switch (true) {
case matchContentType(res, "application/json")
|| jsonLikeContentTypeRE.test(ct) && !jsonlLikeContentTypeRE.test(ct):
logger.log(await res.clone().json());
break;
case matchContentType(res, "application/jsonl")
|| jsonlLikeContentTypeRE.test(ct):
logger.log(await res.clone().text());
break;
case matchContentType(res, "text/event-stream"):
logger.log(`<${contentType}>`);
break;
case matchContentType(res, "text/*"):
logger.log(await res.clone().text());
break;
case matchContentType(res, "multipart/form-data"): {
const body = await res.clone().formData();
for (const [k, v] of body) {
const vlabel = v instanceof Blob ? "<Blob>" : v;
logger.log(`${k}: ${vlabel}`);
}
break;
}
default:
logger.log(`<${contentType}>`);
break;
}
logger.groupEnd();
logger.groupEnd();
}