poe-code 4.0.15 → 4.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,18 @@ import { parse as parseYaml } from "yaml";
4
4
  import { UserError } from "toolcraft";
5
5
  import { renderSourceSnippet } from "toolcraft/source-snippet";
6
6
  import { classifyNetworkError } from "./network-error.js";
7
+ import { redactSensitiveQueryValues } from "./redaction.js";
8
+ export class OpenApiHttpStatusError extends UserError {
9
+ }
10
+ export class OpenApiTransportError extends UserError {
11
+ }
12
+ export class OpenApiTimeoutError extends OpenApiTransportError {
13
+ timeoutMs;
14
+ constructor(message, timeoutMs, options) {
15
+ super(message, options);
16
+ this.timeoutMs = timeoutMs;
17
+ }
18
+ }
7
19
  export async function readOpenApiSourceText(input, services) {
8
20
  const inputUrl = input instanceof URL ? input : tryParseUrl(input);
9
21
  const sourceLabel = formatSourceLabel(input);
@@ -20,29 +32,118 @@ export async function readOpenApiSourceText(input, services) {
20
32
  if (inputUrl.protocol !== "http:" && inputUrl.protocol !== "https:") {
21
33
  throw new UserError(`Unsupported OpenAPI input URL protocol ${JSON.stringify(inputUrl.protocol)}.`);
22
34
  }
35
+ const result = await fetchOpenApiHttpSource(inputUrl, services.fetch);
36
+ if (result.status === "not-modified") {
37
+ throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: received 304 without a cached document.`);
38
+ }
39
+ return result.sourceText;
40
+ }
41
+ catch (error) {
42
+ if (error instanceof UserError) {
43
+ throw error;
44
+ }
45
+ throw new UserError(`Failed to read OpenAPI document ${JSON.stringify(sourceLabel)}: ${getErrorMessage(error)}`);
46
+ }
47
+ }
48
+ export async function fetchOpenApiHttpSource(inputUrl, fetch, options = {}) {
49
+ validateTimeout(options.timeoutMs);
50
+ const url = inputUrl.toString();
51
+ const timeoutMs = options.timeoutMs;
52
+ const controller = timeoutMs === undefined || timeoutMs === 0 ? undefined : new AbortController();
53
+ const requestInit = createOpenApiRequestInit(options.etag, controller?.signal);
54
+ const request = async () => {
23
55
  let response;
24
56
  try {
25
- response = await services.fetch(inputUrl.toString());
57
+ response = requestInit === undefined ? await fetch(url) : await fetch(url, requestInit);
26
58
  }
27
59
  catch (error) {
28
- throw classifyNetworkError(error, inputUrl.toString()) ?? error;
60
+ throw toTransportError(error, url) ?? error;
61
+ }
62
+ if (response.status === 304) {
63
+ return {
64
+ status: "not-modified",
65
+ ...readResponseCacheHeaders(response)
66
+ };
29
67
  }
30
68
  if (!response.ok) {
31
69
  const contentType = response.headers.get("content-type") ?? "";
32
70
  const text = await response.text().catch(() => "");
33
71
  const snippet = text.length === 0 ? "" : `\n body: ${truncate(text, 500)}`;
34
- throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: ` +
72
+ throw new OpenApiHttpStatusError(`Failed to fetch ${JSON.stringify(url)}: ` +
35
73
  `${response.status} ${response.statusText}` +
36
74
  (contentType ? ` (content-type: ${contentType})` : "") +
37
75
  snippet);
38
76
  }
39
- return await response.text();
77
+ try {
78
+ return {
79
+ status: "modified",
80
+ sourceText: await response.text(),
81
+ ...readResponseCacheHeaders(response)
82
+ };
83
+ }
84
+ catch (error) {
85
+ throw (toTransportError(error, url) ??
86
+ new OpenApiTransportError(`Failed to read the OpenAPI response body from ${JSON.stringify(redactSensitiveQueryValues(url))}.`, { cause: error }));
87
+ }
88
+ };
89
+ if (controller === undefined || timeoutMs === undefined) {
90
+ return await request();
40
91
  }
41
- catch (error) {
42
- if (error instanceof UserError) {
43
- throw error;
92
+ const timeoutCause = Object.assign(new Error("OpenAPI request timed out"), {
93
+ code: "ETIMEDOUT",
94
+ timeout: timeoutMs
95
+ });
96
+ const classified = classifyNetworkError(timeoutCause, url);
97
+ const timeoutError = new OpenApiTimeoutError(classified?.message ?? `OpenAPI request timed out after ${timeoutMs}ms.`, timeoutMs, { cause: timeoutCause });
98
+ let timeout;
99
+ try {
100
+ return await Promise.race([
101
+ request(),
102
+ new Promise((_resolve, reject) => {
103
+ timeout = setTimeout(() => {
104
+ reject(timeoutError);
105
+ controller.abort(timeoutError);
106
+ }, timeoutMs);
107
+ })
108
+ ]);
109
+ }
110
+ finally {
111
+ if (timeout !== undefined) {
112
+ clearTimeout(timeout);
44
113
  }
45
- throw new UserError(`Failed to read OpenAPI document ${JSON.stringify(sourceLabel)}: ${getErrorMessage(error)}`);
114
+ }
115
+ }
116
+ function toTransportError(error, url) {
117
+ if (error instanceof OpenApiTransportError) {
118
+ return error;
119
+ }
120
+ const classified = classifyNetworkError(error, url);
121
+ return classified === null
122
+ ? null
123
+ : new OpenApiTransportError(classified.message, { cause: error });
124
+ }
125
+ function createOpenApiRequestInit(etag, signal) {
126
+ if (etag === undefined && signal === undefined) {
127
+ return undefined;
128
+ }
129
+ return {
130
+ ...(etag === undefined ? {} : { headers: { "If-None-Match": etag } }),
131
+ ...(signal === undefined ? {} : { signal })
132
+ };
133
+ }
134
+ function readResponseCacheHeaders(response) {
135
+ const etag = response.headers.get("etag") ?? undefined;
136
+ const cacheControl = response.headers.get("cache-control") ?? undefined;
137
+ const age = response.headers.get("age") ?? undefined;
138
+ return {
139
+ ...(etag === undefined ? {} : { etag }),
140
+ ...(cacheControl === undefined ? {} : { cacheControl }),
141
+ ...(age === undefined ? {} : { age })
142
+ };
143
+ }
144
+ function validateTimeout(timeoutMs) {
145
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
146
+ throw new UserError("OpenAPI fetch timeout must be a finite non-negative number.");
46
147
  }
47
148
  }
48
149
  export function parseOpenApiDocument(sourceText, input) {