All files / blong-gogo/src/adapter/browser http.ts

72.06% Statements 129/179
28% Branches 14/50
75% Functions 6/8
72.06% Lines 129/179

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 1801x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x             1x                             1x   15x 15x 15x 15x 15x 15x                   1x 1x 1x   15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x                 15x 15x 15x 15x 15x 15x 15x 15x 15x   15x 15x 15x 15x 15x   15x   15x 15x 15x 15x 15x 15x 15x 15x             15x 15x     15x 1x 1x  
import type {Adapter, Errors, IErrorMap, IMeta} from '@feasibleone/blong/types';
import {adapter} from '@feasibleone/blong/types';
import ky, {type Options as KyOptions} from 'ky';
 
export interface IConfig {
    tls?: {
        key?: string;
        cert?: string;
        ca?: string | string[];
        crl?: string;
    };
    url?: string;
    /** Alternative to `url`: construct the base URL from these parts.
     *  The `port` value may be a Promise (e.g. from a manifest property)
     *  that resolves to the effective port number. */
    port?: number | Promise<number>;
    host?: string;
    protocol?: string;
}
 
const errorMap: IErrorMap = {
    'http.generic': 'HTTP Error',
};
 
let _errors: Errors<typeof errorMap>;
 
export default adapter<IConfig>(({utError, manifest}) => {
    _errors ||= utError.register(errorMap);
 
    let kyInstance: typeof ky = ky;
    return {
        activation: {
            default: {
                type: 'http',
            },
        },
        async init(...configs: object[]) {
            await super.init(...configs);
            // If port/host/protocol are provided instead of url, and the port
            // value is already a plain number (e.g. from CLI --manifest.gatewayPort=8080),
            // construct the URL straight away.  Promise values (dynamic manifest
            // ports) are resolved lazily on first exec() call.
            if (!this.config.url && this.config.host && this.config.port != null) {
                const portCandidate = this.config.port;
                if (typeof portCandidate === 'number' || typeof portCandidate === 'string') {
                    const protocol = this.config.protocol || 'http';
                    this.config.url = `${protocol}://${this.config.host}:${portCandidate}`;
                }
            }
            if (this.config.tls) {
                // Dynamic imports — only run on the server; @vite-ignore prevents
                // Vite from trying to bundle these Node.js-only modules for the browser.
                const [{Agent}, {readFileSync}] = await Promise.all([
                    import(/* @vite-ignore */ 'undici') as Promise<typeof import('undici')>,
                    import(/* @vite-ignore */ 'node:fs') as Promise<typeof import('node:fs')>,
                ]);
                const {tls} = this.config;
                const agent = new Agent({
                    connect: {
                        minVersion: 'TLSv1.3',
                        ...(tls.key && {key: readFileSync(tls.key)}),
                        ...(tls.cert && {cert: readFileSync(tls.cert)}),
                        ...(tls.ca && {
                            ca: Array.isArray(tls.ca)
                                ? tls.ca.map(f => readFileSync(f))
                                : readFileSync(tls.ca),
                        }),
                        ...(tls.crl && {crl: readFileSync(tls.crl)}),
                    },
                });
                kyInstance = ky.create({
                    fetch: (url, options) =>
                        fetch(url as string, {
                            ...(options as RequestInit),
                            // @ts-expect-error: undici dispatcher is not in the standard RequestInit type
                            dispatcher: agent,
                        }),
                });
            }
        },
        start() {
            super.connect();
            return super.start();
        },
        async exec(
            this: Adapter<IConfig>,
            {
                path,
                query: searchParams,
                url,
                responseType,
                method,
                headers,
                body,
                form,
                json,
            }: {
                path: string;
                query: string | Record<string, string>;
                url?: URL;
                responseType: 'json' | 'text' | 'buffer';
                method: string;
                headers: Record<string, string>;
                body: BodyInit;
                form: Record<string, string>;
                json: unknown;
            },
            $meta: IMeta,
        ) {
            try {
                // Lazily resolve the base URL when not yet available.
                // Resolution priority:
                //   1. Manifest gatewayPort (set by server-side Gateway after start)
                //   2. Config port/host/protocol (from realm factory)
                //   3. Config url (hardcoded fallback like 'http://localhost:8080')
                if (!url) {
                    let baseUrl: string | undefined;
                    if (this.config.host) {
                        // `await` handles plain values, promises and undefined alike
                        const manifestPort = manifest ? await manifest.gatewayPort : undefined;
                        const configPort =
                            this.config.port != null ? await this.config.port : undefined;
                        const resolvedPort = manifestPort ?? configPort;
                        if (resolvedPort != null) {
                            const protocol = this.config.protocol || 'http';
                            baseUrl = `${protocol}://${this.config.host}:${resolvedPort}`;
                            this.config.url = baseUrl;
                        }
                    }
                    url = new URL(path, baseUrl || (this.config.url as string) || '');
                }
                this.log?.debug?.({
                    req: {
                        method: (method || 'POST').toUpperCase(),
                        url,
                        headers,
                        body,
                        json,
                    },
                });
                const kyOptions: KyOptions = {
                    method: method || 'POST',
                    headers,
                    throwHttpErrors: false,
                    redirect: 'manual',
                    ...(json != null ? {json} : {}),
                    ...(form != null ? {body: new URLSearchParams(form)} : {}),
                    ...(body != null && json == null && form == null ? {body} : {}),
                    ...(searchParams != null
                        ? {searchParams: searchParams as Record<string, string>}
                        : {}),
                };
                const res = await kyInstance(url.toString(), kyOptions);
                const resolvedBody =
                    responseType === 'buffer'
                        ? await res.arrayBuffer()
                        : responseType === 'text'
                          ? await res.text()
                          : await res.json().catch(() => null);
                const result = {
                    statusCode: res.status,
                    statusMessage: res.statusText,
                    headers: Object.fromEntries(res.headers.entries()),
                    body: resolvedBody,
                };
                this.log?.debug?.({
                    req: {
                        url,
                        method: (method || 'POST').toUpperCase(),
                    },
                    res: result,
                });
                return result;
            } catch (error) {
                throw this.error(_errors['http.generic'](error), $meta);
            }
        },
    };
});