UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

679 lines • 30.2 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { IOResult, materialize } from "../../../io/types.js"; import { OutputStream, asyncChain, closeQuietly, ensureStream } from "../../../io/stream.js"; import { PathSpec } from "../../../types.js"; import { command } from "../../config.js"; import { specOf } from "../../spec/builtins.js"; import { HttpConnectError, HttpTimeoutError } from "../errors.js"; import { DEFAULT_USER_AGENT, httpFormRequest, httpRequest, isHttpError, } from "../utils/http.js"; import { HTTP_TIMEOUT_SECONDS } from "../utils/constants.js"; import { CookieJar, sameOrigin } from "../utils/cookie.js"; import { resolveTarget } from "../utils/paths.js"; import { UsageError } from "../../errors.js"; import { fsStrerror, isFsError, isWalkError, enotsup } from "../../../errors/fs.js"; import { compareCodePoints } from "../../../utils/sort.js"; import { FlagView } from "../../spec/flag_view.js"; import { encodeBase64 } from "../../../utils/base64.js"; import { renderWriteOut } from "./curl_write_out.js"; import { concat } from "../../../utils/bytes.js"; const ENC = new TextEncoder(); const DEC = new TextDecoder(); /** * Opt in by assigning a debug sink, as Python's module logger is off until * configured: a cookie file curl cannot read or write is skipped silently. */ export const logger = {}; // Exit codes real curl uses for the failures mirage can hit. An HTTP error // status is deliberately absent: curl treats 4xx/5xx as a successful transfer // and prints the body, and only -f/--fail turns it into EXIT_HTTP_ERROR. // 2 is any line curl refuses before a transfer. const EXIT_USAGE = 2; const EXIT_CONNECT = 7; const EXIT_HTTP_ERROR = 22; const EXIT_WRITE = 23; const EXIT_READ = 26; const EXIT_TIMEOUT = 28; const CRLF = '\r\n'; const HELP_HINT = "curl: try 'curl --help' or 'curl --manual' for more information"; // curl 8.7.1's wording, wrapped where it wraps it (the trailing space is the // wrap point). const HEAD_DATA_WARNING = 'Warning: You can only select one HTTP request method! You asked for both POST \n' + 'Warning: (-d, --data) and HEAD (-I, --head).\n'; const HEAD_FORM_WARNING = 'Warning: You can only select one HTTP request method! You asked for both \n' + 'Warning: multipart formpost (-F, --form) and HEAD (-I, --head).\n'; // curl's own Content-Type for a -d body, sent unless the line names one: // httpx's `content=` and fetch's body carry no type of their own. const BODY_CONTENT_TYPE = 'application/x-www-form-urlencoded'; const JSON_TYPE = 'application/json'; // Every option that adds a piece to the body, in the order curl joins them, // and the spelling curl names in a read failure. const DATA_OPTIONS = ['data', 'data_binary', 'data_raw', 'data_urlencode', 'json']; const DATA_SPELLING = { data: '-d', data_binary: '--data-binary', data_urlencode: '--data-urlencode', json: '--json', }; // -d @FILE drops these bytes from what it reads; --data-binary keeps them. const DATA_STRIPPED = new Set([0x0d, 0x0a, 0x00]); const UNRESERVED = /^[A-Za-z0-9\-._~]$/; /** * The headers -H sends, and every name it mentions. * * curl's reading of each line: `Name: value` sends the header, `Name:` with * nothing after it sends nothing and still stops curl adding its own of that * name, `Name;` sends it empty, and a line with neither separator is * dropped. The value loses its leading blanks only. A name given twice in any * case is one header whose values are joined with `, `, a deliberate * divergence from curl, which sends two lines: fetch merges them so, and RFC * 9110 reads the two forms alike. */ export function headerLines(values) { const sent = new Map(); const named = new Set(); for (const line of values) { const colon = line.indexOf(':'); let name; let value; if (colon > 0) { name = line.slice(0, colon); value = line.slice(colon + 1).replace(/^\s+/, ''); } else if (colon < 0 && line.endsWith(';') && line.length > 1) { name = line.slice(0, -1); value = ''; } else continue; const key = name.toLowerCase(); named.add(key); if (colon > 0 && value === '') continue; const prior = sent.get(key); sent.set(key, prior === undefined ? [name, value] : [prior[0], `${prior[1]}, ${value}`]); } return [[...sent.values()], named]; } /** * The request curl -v shows, as far as mirage can see it. * * Only what leaves mirage is dumped: the request line, Host, the headers in * curl's order, and the two a -d body adds. curl's `*` transport lines * (resolving, connecting, TLS) have no source here and are omitted, as are * the headers the HTTP stack appends on its own and a -F body's encoding, * which the client builds. `headers` is every header between Host and the * body's own, in the order curl sends them; `bodyType` is the Content-Type * curl added for the body itself, null when the line named one or there is * no body. */ export function requestLines(url, method, headers, bodyLen, bodyType) { let target = url; let host = ''; try { const parsed = new URL(url); target = `${parsed.pathname}${parsed.search}`; host = parsed.port !== '' ? `${parsed.hostname}:${parsed.port}` : parsed.hostname; } catch { // Not a URL fetch could parse either; the request line shows the word. } const lines = [`${method} ${target} HTTP/1.1`, `Host: ${host}`]; for (const [k, v] of headers) lines.push(`${k}: ${v}`); if (bodyLen !== null) lines.push(`Content-Length: ${String(bodyLen)}`); if (bodyType !== null) lines.push(`Content-Type: ${bodyType}`); return lines; } /** * curl's escaping for --data-urlencode: everything but letters, digits and * `-._~` becomes `%XX`, and a space then becomes `+` (curl 8.14.1). */ export function urlEncoded(data) { let out = ''; for (const byte of data) { const char = String.fromCharCode(byte); out += UNRESERVED.test(char) ? char : byte === 0x20 ? '+' : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`; } return out; } /** * Read the file an `@NAME` value names, `-` being stdin. curl gives up on * the whole line when it cannot (exit 26), naming the file unless -s, and * the option either way. */ async function readAt(option, name, ctx, silent) { try { if (name === '-') return await materialize(ctx.stdin); if (ctx.vfs === undefined) throw enotsup('unavailable', 'read', name); const [content] = await ctx.vfs.dispatch('read', resolveTarget(name, ctx.cwd.virtual), []); return await materialize(content); } catch (err) { if (!isWalkError(err)) throw err; const detail = silent ? '' : `curl: Failed to open ${name}\n`; const failure = new UsageError(`${detail}curl: option ${option}: error encountered when reading a file\n${HELP_HINT}`, EXIT_READ); failure.cause = err; throw failure; } } /** * The text of the cookie file a -b value names, `-` being stdin, and none for * an empty name. curl goes on without a file it cannot read, saying nothing * (curl 8.14.1). */ async function readCookies(name, ctx) { if (name === '') return ''; try { if (name === '-') return DEC.decode(await materialize(ctx.stdin)); if (ctx.vfs === undefined) return ''; const [content] = await ctx.vfs.dispatch('read', resolveTarget(name, ctx.cwd.virtual), []); return DEC.decode(await materialize(content)); } catch (err) { if (!isWalkError(err)) throw err; logger.debug?.(`curl: cookie file ${name} not read: ${String(err)}`); return ''; } } /** * Write the jar to the file -c names, once the transfer has run, whatever it * ended in; what stdout gets for `-`, which comes after everything else. A * file curl cannot write stays unwritten, without a word (curl 8.14.1). */ async function saveCookies(ctx, target, jar) { const text = ENC.encode(jar.render()); if (target === '-' || (target instanceof PathSpec && target.rawPath === '-')) return text; if (ctx.vfs === undefined) return new Uint8Array(); try { await ctx.vfs.dispatch('write', resolveTarget(target, ctx.cwd.virtual), [text]); } catch (err) { if (!isWalkError(err)) throw err; logger.debug?.(`curl: cookie jar ${pathStr(target)} not written: ${String(err)}`); } return new Uint8Array(); } /** One data option's contribution to the body. */ async function dataPiece(kind, value, ctx, silent) { if (kind === 'data_raw') return ENC.encode(value); const option = DATA_SPELLING[kind] ?? kind; if (kind === 'data_urlencode') { // `=` is looked for before `@`: `name=content` encodes the content, // `name@file` a file's, and a bare value all of it. let sep = value.indexOf('='); if (sep < 0) sep = value.indexOf('@'); const name = sep > 0 ? value.slice(0, sep) : ''; const content = sep >= 0 ? value.slice(sep + 1) : value; const raw = sep >= 0 && value[sep] === '@' ? await readAt(option, content, ctx, silent) : ENC.encode(content); const encoded = urlEncoded(raw); return ENC.encode(name !== '' ? `${name}=${encoded}` : encoded); } if (!value.startsWith('@')) return ENC.encode(value); const raw = await readAt(option, value.slice(1), ctx, silent); return kind === 'data' ? raw.filter((byte) => !DATA_STRIPPED.has(byte)) : raw; } /** * The body the data options build, and whether --json was among them. The * pieces are joined in line order with `&` between them, except before a * --json piece, and only once the body so far is not empty (curl 8.14.1: * `-d '' -d b` sends `b`). */ async function requestBody(fl, ctx) { const silent = fl.asBool('silent'); const occurrences = fl.occurrences(...DATA_OPTIONS); if (occurrences.length === 0) return [null, false]; let body = new Uint8Array(); let json = false; for (const [kind, value] of occurrences) { const piece = await dataPiece(kind, String(value), ctx, silent); if (body.length > 0 && kind !== 'json') body = concat([body, ENC.encode('&')]); body = concat([body, piece]); json ||= kind === 'json'; } return [body, json]; } /** * The Authorization value -u sends, and the prompt it costs. A user without * a `:` has curl ask for the password on stderr and read it from stdin, * which is all a workspace has: the bytes it read minus the last one, which * curl takes to be the newline. */ export function basicAuth(user, stdin) { let prompt = new Uint8Array(); let pair = user; if (!user.includes(':')) { prompt = ENC.encode(`Enter host password for user '${user}':\n`); pair = `${user}:${new TextDecoder().decode(stdin.slice(0, -1))}`; } return [`Basic ${encodeBase64(ENC.encode(pair))}`, prompt]; } /** * The status line and headers -i, -I and -v print. * * Three deliberate divergences from curl, which prints the bytes as * received: names render lowercase, because fetch never exposes the wire * casing; they come sorted by name, because the Headers class iterates * that way and wire order is gone by then; and the version always reads * HTTP/1.1, because fetch cannot observe it. Both hosts therefore print * one shape. */ export function responseLines(resp) { const lines = [`HTTP/1.1 ${String(resp.status)} ${resp.reason}`]; const sorted = resp.headers.map(([k, v]) => [k.toLowerCase(), v]); sorted.sort(([a], [b]) => compareCodePoints(a, b)); for (const [k, v] of sorted) lines.push(`${k}: ${v}`); return lines; } function dump(lines, prefix = '') { return [...lines, ''].map((line) => `${prefix}${line}${CRLF}`).join(''); } /** * The header blocks when -D and -i (or -I) both print to stdout. curl * writes each header line to the dump as it arrives and then to the * output, so on one stream every line comes out twice, one after the other * (curl 8.14.1). */ function doubled(hops) { return hops .flatMap((hop) => [...responseLines(hop), '']) .map((line) => `${line}${CRLF}${line}${CRLF}`) .join(''); } /** * Why a write to `shown` failed, in curl's exit code 23. * * Deliberate divergence: real curl says "Failed writing received data to * disk/application" (or "client returned ERROR on write of N bytes") and * drops the cause. A mirage write can fail for reasons a local file cannot * (read-only mount, unsupported op), so the exit code matches curl while the * message keeps path and reason. The refusals whose wording is load-bearing * (read-only mount, unsupported op) keep their raw message; an unusable path * carries only the path as its message, so it needs the strerror text. */ function writeFailure(shown, err) { const code = err.code; const strerror = fsStrerror(err); const raw = code === 'EACCES' || code === 'ENOTSUP' || !isFsError(err); const detail = !raw && strerror !== null ? strerror : err instanceof Error ? err.message : String(err); return `curl: (${String(EXIT_WRITE)}) ${shown}: ${detail}\n`; } function pathStr(value) { return value instanceof PathSpec ? value.virtual : value; } /** A message curl or wget prints, nothing when the line silenced it. */ export function said(quiet, text) { return quiet ? new Uint8Array() : ENC.encode(text); } // Write a -D or -o file through the dispatcher: null once written (or with no // file handle to write through), else the failure's stderr. Mirrors Python's // _write_named. // curl's message for a transfer past its deadline: the bytes that arrived, out // of the size the server announced when it announced one. function timedOut(err, received, response) { const names = new Map((response?.headers ?? []).map(([k, v]) => [k.toLowerCase(), v])); // The announced size counts the bytes on the wire, which a decoded body no // longer matches. const total = names.has('content-encoding') ? undefined : names.get('content-length'); const of = total === undefined ? '' : ` out of ${total}`; return `curl: (${String(EXIT_TIMEOUT)}) Operation timed out after ${String(err.elapsedMs)} milliseconds with ${String(received)}${of} bytes received\n`; } function unreachable(err) { return `curl: (${String(EXIT_CONNECT)}) Failed to connect to ${err.host} port ${String(err.port)}: Could not connect to server\n`; } async function writeNamed(ctx, target, data, quiet) { if (ctx.vfs === undefined) return null; try { await ctx.vfs.dispatch('write', resolveTarget(target, ctx.cwd.virtual), [data]); } catch (err) { if (!isWalkError(err)) throw err; return said(quiet, writeFailure(pathStr(target), err)); } return null; } async function curl(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags, specOf('curl')); const userAgent = fl.asStr('user_agent') ?? null; const request = fl.asStr('request') ?? null; const hasData = fl.occurrences(...DATA_OPTIONS).length > 0; const form = fl.asStr('form') ?? null; const output = fl.asPath('output') ?? fl.asStr('output') ?? null; const outputIsStdout = output === '-' || (output instanceof PathSpec && output.rawPath === '-'); const dumpHeader = fl.asPath('dump_header') ?? fl.asStr('dump_header') ?? null; const dumpToStdout = dumpHeader === '-' || (dumpHeader instanceof PathSpec && dumpHeader.rawPath === '-'); const dumpFile = dumpToStdout ? null : dumpHeader; // -k skips certificate verification through the fetch the host registered // (utils/http.ts); the browser has none, so there certificates are still // verified. const verify = !fl.asBool('insecure'); const location = fl.asBool('location'); const failOnError = fl.asBool('fail'); const verbose = fl.asBool('verbose'); const include = fl.asBool('include'); const head = fl.asBool('head'); const maxTime = fl.asFloat('max_time'); // -s silences the message, -S puts it back. Neither changes the exit code. const quiet = fl.asBool('silent') && !fl.asBool('show_error'); const [sent, named] = headerLines(fl.asList('header')); // -b data with a `=` goes as given, after the jar's cookies; anything else // names a cookie file, which, like -c, turns the jar on (curl 8.14.1). A // Cookie header the line typed takes the data's place. const cookieJar = fl.asPath('cookie_jar') ?? fl.asStr('cookie_jar') ?? null; const cookieArgs = fl.asList('cookie'); const literals = cookieArgs.filter((value) => value.includes('=')); const cookieFiles = cookieArgs.filter((value) => !value.includes('=')); const jar = new CookieJar(cookieFiles.length > 0 || cookieJar !== null, literals.length === 0 || named.has('cookie') ? null : literals.map((value) => value.trimStart()).join(';')); // curl refuses these lines before any transfer (curl 8.7.1, exit 2). A // negative --max-time is not a number curl takes; curl names the spelling // typed, which the handler cannot see, so the long one. if (maxTime !== undefined && maxTime < 0) { throw new UsageError(`curl: option --max-time: expected a positive numerical parameter\n${HELP_HINT}`, EXIT_USAGE); } // -I beside a body option asks two methods of one request: curl warns and // refuses. -s mutes the warning and -S does not bring it back; the option // error -F adds is never muted. if (head && (hasData || form !== null)) { let err = fl.asBool('silent') ? '' : hasData ? HEAD_DATA_WARNING : HEAD_FORM_WARNING; if (!hasData) err += `curl: option -F: is badly used here\n${HELP_HINT}\n`; return [null, new IOResult({ exitCode: EXIT_USAGE, stderr: ENC.encode(err) })]; } const url = texts[0]; if (url === undefined) { throw new UsageError(`curl: (2) no URL specified\n${HELP_HINT}`, EXIT_USAGE); } // A zero --max-time is curl's "no limit", not a deadline of zero. const timeoutMs = maxTime === undefined ? HTTP_TIMEOUT_SECONDS * 1000 : maxTime === 0 ? null : maxTime * 1000; let template = fl.asStr('write_out') ?? ''; // curl 8.14.1: -s suppresses only the opening diagnostic; -S does not // restore it. The option error is always printed. Parsed flags lose their // spelling, so each option names its usual one. if (template.startsWith('@')) { const raw = await readAt('-w', template.slice(1), ctx, fl.asBool('silent')); template = new TextDecoder().decode(raw); } const [body, json] = await requestBody(fl, ctx); // A custom header of a name curl would add itself takes its place, so // curl's own goes only where the line names none. The trace lists them in // curl's order; the wire leaves User-Agent and Accept to the client's // defaults unless the line set them. const auth = []; let prompt = new Uint8Array(); const user = fl.asStr('user') ?? null; if (user !== null && !named.has('authorization')) { const stdin = user.includes(':') ? new Uint8Array() : await materialize(ctx.stdin); const [token, asked] = basicAuth(user, stdin); prompt = asked; auth.push(['Authorization', token]); } const agent = userAgent !== null && !named.has('user-agent') ? [['User-Agent', userAgent]] : []; const typed = json ? ['Content-Type', 'Accept'] .filter((name) => !named.has(name.toLowerCase())) .map((name) => [name, JSON_TYPE]) : []; const headers = Object.fromEntries([...auth, ...agent, ...sent, ...typed]); // The jar's Cookie line goes between the two, as each hop sent it. const traceHead = [ ...auth, ...(named.has('user-agent') ? [] : [['User-Agent', userAgent ?? DEFAULT_USER_AGENT]]), ...(named.has('accept') || json ? [] : [['Accept', '*/*']]), ]; const traceTail = [...sent, ...typed]; const started = performance.now(); // The body bytes read so far, and the failure that cut the transfer short: // the status, the message and -w report both once it ends. let received = 0; let cut = null; // Written once, when the transfer ends or when its reader lets it go // first, as curl writes the jar at cleanup either way. let jarKept = false; const keepJar = async () => { if (cookieJar === null || jarKept) return new Uint8Array(); jarKept = true; return await saveCookies(ctx, cookieJar, jar); }; const settle = async (io, response) => { if (cut !== null) { let line; if (cut instanceof HttpTimeoutError) { io.exitCode = EXIT_TIMEOUT; line = timedOut(cut, received, response ?? null); } else { io.exitCode = EXIT_CONNECT; line = unreachable(cut); } io.stderr = concat([await materialize(io.stderr), said(quiet, line)]); } const code = String(response?.status ?? 0).padStart(3, '0'); const [out, err] = renderWriteOut(template, { http_code: code, response_code: code, url_effective: response?.url ?? url, num_redirects: String(response?.history.length ?? 0), size_download: String(received), content_type: response?.headers.find(([k]) => k.toLowerCase() === 'content-type')?.[1] ?? '', method: response?.method ?? request ?? (head ? 'HEAD' : hasData || form !== null ? 'POST' : 'GET'), exitcode: String(io.exitCode), time_total: ((performance.now() - started) / 1000).toFixed(6), }); io.stderr = concat([prompt, await materialize(io.stderr), err]); return concat([out, await keepJar()]); }; const finish = async (stdout, io, response) => { if (stdout === null || stdout instanceof Uint8Array) { return [concat([await materialize(stdout), await settle(io, response)]), io]; } // The body streams out first; what curl prints after it waits for its // end, which is when the size and any failure are known. async function* rest() { yield await settle(io, response); } const letGo = async () => { await keepJar(); }; return [asyncChain([stdout, new OutputStream(rest(), letGo)]), io]; }; let method; let bodyLen = null; let bodyType = null; let resp; for (const name of cookieFiles) jar.load(await readCookies(name, ctx)); try { if (form !== null) { method = request ?? 'POST'; const eq = form.indexOf('='); const key = eq >= 0 ? form.slice(0, eq) : form; const value = eq >= 0 ? form.slice(eq + 1) : ''; resp = await httpFormRequest(url, { method, formData: { [key]: value }, headers, timeoutMs, ...(ctx.signal === undefined ? {} : { signal: ctx.signal }), followRedirects: location, verify, cookies: jar, stream: true, }); } else { method = request ?? (head ? 'HEAD' : body !== null ? 'POST' : 'GET'); bodyLen = body !== null ? body.length : null; // -v shows what is sent, so curl's default for the body goes on the // request, not on the trace alone. if (body !== null && !json && !named.has('content-type')) bodyType = BODY_CONTENT_TYPE; const wire = bodyType !== null ? { ...headers, 'Content-Type': bodyType } : headers; resp = await httpRequest(url, { method, headers: wire, ...(body !== null ? { body } : {}), timeoutMs, ...(ctx.signal === undefined ? {} : { signal: ctx.signal }), followRedirects: location, verify, cookies: jar, stream: true, }); } } catch (err) { if (err instanceof HttpTimeoutError) { const line = timedOut(err, 0, null); return await finish(null, new IOResult({ exitCode: EXIT_TIMEOUT, stderr: said(quiet, line) })); } if (!(err instanceof HttpConnectError)) throw err; return await finish(null, new IOResult({ exitCode: EXIT_CONNECT, stderr: said(quiet, unreachable(err)) })); } const final = resp; // The final body as it arrives, counted for -w. A transfer that breaks off // keeps what came, as curl's output does. async function* delivered() { try { for await (const chunk of ensureStream(final.body)) { received += chunk.byteLength; yield chunk; } } catch (err) { if (!(err instanceof HttpConnectError)) throw err; cut = err; } } // Closing it lets the connection go, whether or not it was ever read. const bodyOut = new OutputStream(delivered(), () => closeQuietly(final.body)); const hops = [...resp.history, resp]; // The first request is the one this handler built; each redirect's is // the one the client reports, at the URL the server named. const requests = [ [url, method], ...hops.slice(1).map((hop) => [hop.url, hop.method]), ]; // -v is not a message, so -s leaves it alone. A followed redirect is a // request of its own, traced in turn; the body rides only the hops that // kept the method (a 302 turns a POST into a GET without one). const trace = verbose ? ENC.encode(hops .map((hop, index) => { const [target, sentAs] = requests[index] ?? [hop.url, hop.method]; const carries = sentAs === method; const cookie = jar.sent[index] ?? null; // A typed Cookie header goes to the first origin alone. const tail = sameOrigin(hop.url, hops[0]?.url ?? url) ? traceTail : traceTail.filter(([name]) => name.toLowerCase() !== 'cookie'); return (dump(requestLines(target, sentAs, [ ...traceHead, ...(cookie === null ? [] : [['Cookie', cookie]]), ...tail, ], carries ? bodyLen : null, carries ? bodyType : null), '> ') + dump(responseLines(hop), '< ')); }) .join('')) : new Uint8Array(); // -i, -I and -D all show every hop's header block (curl 8.14.1); the body // a redirect carried is never written, only the final one. const blocks = ENC.encode(hops.map((hop) => dump(responseLines(hop))).join('')); // -D writes the headers as they arrive, so before -f judges the status and // before -o writes the body: a file both name ends up holding the body. if (dumpFile !== null) { const refused = await writeNamed(ctx, dumpFile, blocks, quiet); if (refused !== null) { await closeQuietly(bodyOut); return await finish(null, new IOResult({ exitCode: EXIT_WRITE, stderr: concat([trace, refused]) }), resp); } } const headerOut = dumpToStdout ? blocks : null; // Only -f makes an error status an error, and then no body is written; the // headers -D already dumped stay dumped. if (failOnError && isHttpError(resp)) { await closeQuietly(bodyOut); const line = `curl: (${String(EXIT_HTTP_ERROR)}) The requested URL returned error: ${String(resp.status)}\n`; return await finish(headerOut, new IOResult({ exitCode: EXIT_HTTP_ERROR, stderr: concat([trace, said(quiet, line)]), }), resp); } let result = bodyOut; if (head) { // -I prints the headers alone, whatever method -X made it send. await closeQuietly(bodyOut); result = blocks; } else if (include) { result = asyncChain([blocks, bodyOut]); } if (output !== null && !outputIsStdout) { const refused = await writeNamed(ctx, output, result, quiet); await closeQuietly(result); await closeQuietly(bodyOut); if (refused !== null) { return await finish(headerOut, new IOResult({ exitCode: EXIT_WRITE, stderr: concat([trace, refused]) }), resp); } // Real curl writes the body to the file and prints nothing else on // stdout, the headers -D sends there aside. return await finish(headerOut, new IOResult({ stderr: trace }), resp); } if (dumpToStdout) { result = head || include ? asyncChain([ENC.encode(doubled(hops)), head ? null : bodyOut]) : asyncChain([blocks, result]); } return await finish(result, new IOResult({ stderr: trace }), resp); } export const GENERAL_CURL = command({ name: 'curl', vfs: null, spec: specOf('curl'), write: true, fn: curl, }); //# sourceMappingURL=curl.js.map