@pierre/storage 0.0.11 → 0.1.1
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.
- package/README.md +159 -78
- package/dist/index.cjs +739 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +560 -88
- package/dist/index.d.ts +560 -88
- package/dist/index.js +738 -73
- package/dist/index.js.map +1 -1
- package/package.json +39 -37
- package/src/commit.ts +196 -43
- package/src/errors.ts +50 -0
- package/src/fetch.ts +75 -5
- package/src/index.ts +389 -47
- package/src/schemas.ts +138 -0
- package/src/types.ts +182 -89
- package/src/util.ts +0 -18
- package/src/webhook.ts +75 -3
package/src/commit.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { inferRefUpdateReason, RefUpdateError } from './errors';
|
|
2
|
+
import type { CommitPackAckRaw } from './schemas';
|
|
3
|
+
import { commitPackAckSchema, commitPackResponseSchema, errorEnvelopeSchema } from './schemas';
|
|
1
4
|
import type {
|
|
5
|
+
BlobLike,
|
|
2
6
|
CommitBuilder,
|
|
3
7
|
CommitFileOptions,
|
|
4
8
|
CommitFileSource,
|
|
5
|
-
|
|
9
|
+
CommitResult,
|
|
6
10
|
CommitTextFileOptions,
|
|
7
11
|
CreateCommitOptions,
|
|
12
|
+
ReadableStreamLike,
|
|
13
|
+
RefUpdate,
|
|
8
14
|
} from './types';
|
|
9
15
|
|
|
10
16
|
const MAX_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
@@ -21,37 +27,22 @@ const BufferCtor: NodeBufferConstructor | undefined = (
|
|
|
21
27
|
globalThis as { Buffer?: NodeBufferConstructor }
|
|
22
28
|
).Buffer;
|
|
23
29
|
|
|
24
|
-
interface ReadableStreamReaderLike<T> {
|
|
25
|
-
read(): Promise<{ value?: T; done: boolean }>;
|
|
26
|
-
releaseLock?(): void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ReadableStreamLike<T> {
|
|
30
|
-
getReader(): ReadableStreamReaderLike<T>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface BlobLike {
|
|
34
|
-
stream(): unknown;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
30
|
type ChunkSegment = {
|
|
38
31
|
chunk: Uint8Array;
|
|
39
32
|
eof: boolean;
|
|
40
33
|
};
|
|
41
34
|
|
|
42
35
|
interface CommitMetadataPayload {
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
target_branch: string;
|
|
37
|
+
expected_head_sha?: string;
|
|
45
38
|
commit_message: string;
|
|
46
|
-
author
|
|
39
|
+
author: {
|
|
47
40
|
name: string;
|
|
48
41
|
email: string;
|
|
49
|
-
date?: string;
|
|
50
42
|
};
|
|
51
43
|
committer?: {
|
|
52
44
|
name: string;
|
|
53
45
|
email: string;
|
|
54
|
-
date?: string;
|
|
55
46
|
};
|
|
56
47
|
files: Array<{
|
|
57
48
|
path: string;
|
|
@@ -69,7 +60,7 @@ interface CommitTransportRequest {
|
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
interface CommitTransport {
|
|
72
|
-
send(request: CommitTransportRequest): Promise<
|
|
63
|
+
send(request: CommitTransportRequest): Promise<CommitPackAck>;
|
|
73
64
|
}
|
|
74
65
|
|
|
75
66
|
interface CommitBuilderDeps {
|
|
@@ -86,6 +77,8 @@ type FileOperationState = {
|
|
|
86
77
|
streamFactory?: () => AsyncIterable<Uint8Array>;
|
|
87
78
|
};
|
|
88
79
|
|
|
80
|
+
type CommitPackAck = CommitPackAckRaw;
|
|
81
|
+
|
|
89
82
|
export class CommitBuilderImpl implements CommitBuilder {
|
|
90
83
|
private readonly options: CreateCommitOptions;
|
|
91
84
|
private readonly getAuthToken: () => Promise<string>;
|
|
@@ -98,18 +91,31 @@ export class CommitBuilderImpl implements CommitBuilder {
|
|
|
98
91
|
this.getAuthToken = deps.getAuthToken;
|
|
99
92
|
this.transport = deps.transport;
|
|
100
93
|
|
|
101
|
-
const trimmedTarget = this.options.
|
|
94
|
+
const trimmedTarget = this.options.targetBranch?.trim();
|
|
102
95
|
const trimmedMessage = this.options.commitMessage?.trim();
|
|
96
|
+
const trimmedAuthorName = this.options.author?.name?.trim();
|
|
97
|
+
const trimmedAuthorEmail = this.options.author?.email?.trim();
|
|
98
|
+
|
|
103
99
|
if (!trimmedTarget) {
|
|
104
|
-
throw new Error('createCommit
|
|
100
|
+
throw new Error('createCommit targetBranch is required');
|
|
101
|
+
}
|
|
102
|
+
if (trimmedTarget.startsWith('refs/')) {
|
|
103
|
+
throw new Error('createCommit targetBranch must not include refs/ prefix');
|
|
105
104
|
}
|
|
106
105
|
if (!trimmedMessage) {
|
|
107
106
|
throw new Error('createCommit commitMessage is required');
|
|
108
107
|
}
|
|
109
|
-
|
|
108
|
+
if (!trimmedAuthorName || !trimmedAuthorEmail) {
|
|
109
|
+
throw new Error('createCommit author name and email are required');
|
|
110
|
+
}
|
|
111
|
+
this.options.targetBranch = trimmedTarget;
|
|
110
112
|
this.options.commitMessage = trimmedMessage;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
this.options.author = {
|
|
114
|
+
name: trimmedAuthorName,
|
|
115
|
+
email: trimmedAuthorEmail,
|
|
116
|
+
};
|
|
117
|
+
if (typeof this.options.expectedHeadSha === 'string') {
|
|
118
|
+
this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -135,11 +141,21 @@ export class CommitBuilderImpl implements CommitBuilder {
|
|
|
135
141
|
contents: string,
|
|
136
142
|
options?: CommitTextFileOptions,
|
|
137
143
|
): CommitBuilder {
|
|
138
|
-
const encoding = options?.encoding;
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
const encoding = options?.encoding ?? 'utf8';
|
|
145
|
+
const normalizedEncoding = encoding === 'utf-8' ? 'utf8' : encoding;
|
|
146
|
+
let data: Uint8Array;
|
|
147
|
+
if (normalizedEncoding === 'utf8') {
|
|
148
|
+
data = new TextEncoder().encode(contents);
|
|
149
|
+
} else if (BufferCtor) {
|
|
150
|
+
data = BufferCtor.from(
|
|
151
|
+
contents,
|
|
152
|
+
normalizedEncoding as Parameters<NodeBufferConstructor['from']>[1],
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`,
|
|
157
|
+
);
|
|
141
158
|
}
|
|
142
|
-
const data = new TextEncoder().encode(contents);
|
|
143
159
|
return this.addFile(path, data, options);
|
|
144
160
|
}
|
|
145
161
|
|
|
@@ -154,7 +170,7 @@ export class CommitBuilderImpl implements CommitBuilder {
|
|
|
154
170
|
return this;
|
|
155
171
|
}
|
|
156
172
|
|
|
157
|
-
async send(): Promise<
|
|
173
|
+
async send(): Promise<CommitResult> {
|
|
158
174
|
this.ensureNotSent();
|
|
159
175
|
this.sent = true;
|
|
160
176
|
|
|
@@ -167,12 +183,13 @@ export class CommitBuilderImpl implements CommitBuilder {
|
|
|
167
183
|
}));
|
|
168
184
|
|
|
169
185
|
const authorization = await this.getAuthToken();
|
|
170
|
-
|
|
186
|
+
const ack = await this.transport.send({
|
|
171
187
|
authorization,
|
|
172
188
|
signal: this.options.signal,
|
|
173
189
|
metadata,
|
|
174
190
|
blobs: blobEntries,
|
|
175
191
|
});
|
|
192
|
+
return buildCommitResult(ack);
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
private buildMetadata(): CommitMetadataPayload {
|
|
@@ -189,19 +206,23 @@ export class CommitBuilderImpl implements CommitBuilder {
|
|
|
189
206
|
});
|
|
190
207
|
|
|
191
208
|
const metadata: CommitMetadataPayload = {
|
|
192
|
-
|
|
209
|
+
target_branch: this.options.targetBranch,
|
|
193
210
|
commit_message: this.options.commitMessage,
|
|
211
|
+
author: {
|
|
212
|
+
name: this.options.author.name,
|
|
213
|
+
email: this.options.author.email,
|
|
214
|
+
},
|
|
194
215
|
files,
|
|
195
216
|
};
|
|
196
217
|
|
|
197
|
-
if (this.options.
|
|
198
|
-
metadata.
|
|
199
|
-
}
|
|
200
|
-
if (this.options.author) {
|
|
201
|
-
metadata.author = { ...this.options.author };
|
|
218
|
+
if (this.options.expectedHeadSha) {
|
|
219
|
+
metadata.expected_head_sha = this.options.expectedHeadSha;
|
|
202
220
|
}
|
|
203
221
|
if (this.options.committer) {
|
|
204
|
-
metadata.committer = {
|
|
222
|
+
metadata.committer = {
|
|
223
|
+
name: this.options.committer.name,
|
|
224
|
+
email: this.options.committer.email,
|
|
225
|
+
};
|
|
205
226
|
}
|
|
206
227
|
|
|
207
228
|
return metadata;
|
|
@@ -229,7 +250,7 @@ export class FetchCommitTransport implements CommitTransport {
|
|
|
229
250
|
this.url = `${trimmedBase}/api/v${config.version}/repos/commit-pack`;
|
|
230
251
|
}
|
|
231
252
|
|
|
232
|
-
async send(request: CommitTransportRequest): Promise<
|
|
253
|
+
async send(request: CommitTransportRequest): Promise<CommitPackAck> {
|
|
233
254
|
const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
|
|
234
255
|
const body = toRequestBody(bodyIterable);
|
|
235
256
|
|
|
@@ -245,11 +266,16 @@ export class FetchCommitTransport implements CommitTransport {
|
|
|
245
266
|
});
|
|
246
267
|
|
|
247
268
|
if (!response.ok) {
|
|
248
|
-
const
|
|
249
|
-
throw new
|
|
269
|
+
const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(response);
|
|
270
|
+
throw new RefUpdateError(statusMessage, {
|
|
271
|
+
status: statusLabel,
|
|
272
|
+
message: statusMessage,
|
|
273
|
+
refUpdate,
|
|
274
|
+
});
|
|
250
275
|
}
|
|
251
276
|
|
|
252
|
-
|
|
277
|
+
const ack = commitPackAckSchema.parse(await response.json());
|
|
278
|
+
return ack;
|
|
253
279
|
}
|
|
254
280
|
}
|
|
255
281
|
|
|
@@ -302,6 +328,36 @@ function buildMessageIterable(
|
|
|
302
328
|
};
|
|
303
329
|
}
|
|
304
330
|
|
|
331
|
+
function buildCommitResult(ack: CommitPackAck): CommitResult {
|
|
332
|
+
const refUpdate = toRefUpdate(ack.result);
|
|
333
|
+
if (!ack.result.success) {
|
|
334
|
+
throw new RefUpdateError(
|
|
335
|
+
ack.result.message ?? `Commit failed with status ${ack.result.status}`,
|
|
336
|
+
{
|
|
337
|
+
status: ack.result.status,
|
|
338
|
+
message: ack.result.message,
|
|
339
|
+
refUpdate,
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
commitSha: ack.commit.commit_sha,
|
|
345
|
+
treeSha: ack.commit.tree_sha,
|
|
346
|
+
targetBranch: ack.commit.target_branch,
|
|
347
|
+
packBytes: ack.commit.pack_bytes,
|
|
348
|
+
blobCount: ack.commit.blob_count,
|
|
349
|
+
refUpdate,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function toRefUpdate(result: CommitPackAck['result']): RefUpdate {
|
|
354
|
+
return {
|
|
355
|
+
branch: result.branch,
|
|
356
|
+
oldSha: result.old_sha,
|
|
357
|
+
newSha: result.new_sha,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
305
361
|
async function* chunkify(source: AsyncIterable<Uint8Array>): AsyncIterable<ChunkSegment> {
|
|
306
362
|
let pending: Uint8Array | null = null;
|
|
307
363
|
let produced = false;
|
|
@@ -369,6 +425,10 @@ async function* toAsyncIterable(source: CommitFileSource): AsyncIterable<Uint8Ar
|
|
|
369
425
|
return;
|
|
370
426
|
}
|
|
371
427
|
}
|
|
428
|
+
if (isReadableStreamLike(source)) {
|
|
429
|
+
yield* readReadableStream(source);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
372
432
|
if (isAsyncIterable(source)) {
|
|
373
433
|
for await (const chunk of source as AsyncIterable<unknown>) {
|
|
374
434
|
yield ensureUint8Array(chunk);
|
|
@@ -492,5 +552,98 @@ export function createCommitBuilder(deps: CommitBuilderDeps): CommitBuilder {
|
|
|
492
552
|
}
|
|
493
553
|
|
|
494
554
|
export function resolveCommitTtlSeconds(options?: { ttl?: number }): number {
|
|
495
|
-
|
|
555
|
+
if (typeof options?.ttl === 'number' && options.ttl > 0) {
|
|
556
|
+
return options.ttl;
|
|
557
|
+
}
|
|
558
|
+
return DEFAULT_TTL_SECONDS;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function parseCommitPackError(response: Response): Promise<{
|
|
562
|
+
statusMessage: string;
|
|
563
|
+
statusLabel: string;
|
|
564
|
+
refUpdate?: Partial<RefUpdate>;
|
|
565
|
+
}> {
|
|
566
|
+
const fallbackMessage = `createCommit request failed (${response.status} ${response.statusText})`;
|
|
567
|
+
const cloned = response.clone();
|
|
568
|
+
let jsonBody: unknown;
|
|
569
|
+
try {
|
|
570
|
+
jsonBody = await cloned.json();
|
|
571
|
+
} catch {
|
|
572
|
+
jsonBody = undefined;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
let textBody: string | undefined;
|
|
576
|
+
if (jsonBody === undefined) {
|
|
577
|
+
try {
|
|
578
|
+
textBody = await response.text();
|
|
579
|
+
} catch {
|
|
580
|
+
textBody = undefined;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const defaultStatus = (() => {
|
|
585
|
+
const inferred = inferRefUpdateReason(String(response.status));
|
|
586
|
+
return inferred === 'unknown' ? 'failed' : inferred;
|
|
587
|
+
})();
|
|
588
|
+
let statusLabel = defaultStatus;
|
|
589
|
+
let refUpdate: Partial<RefUpdate> | undefined;
|
|
590
|
+
let message: string | undefined;
|
|
591
|
+
|
|
592
|
+
if (jsonBody !== undefined) {
|
|
593
|
+
const parsedResponse = commitPackResponseSchema.safeParse(jsonBody);
|
|
594
|
+
if (parsedResponse.success) {
|
|
595
|
+
const result = parsedResponse.data.result;
|
|
596
|
+
if (typeof result.status === 'string' && result.status.trim() !== '') {
|
|
597
|
+
statusLabel = result.status.trim() as typeof statusLabel;
|
|
598
|
+
}
|
|
599
|
+
refUpdate = toPartialRefUpdateFields(result.branch, result.old_sha, result.new_sha);
|
|
600
|
+
if (typeof result.message === 'string' && result.message.trim() !== '') {
|
|
601
|
+
message = result.message.trim();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!message) {
|
|
606
|
+
const parsedError = errorEnvelopeSchema.safeParse(jsonBody);
|
|
607
|
+
if (parsedError.success) {
|
|
608
|
+
const trimmed = parsedError.data.error.trim();
|
|
609
|
+
if (trimmed) {
|
|
610
|
+
message = trimmed;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!message && typeof jsonBody === 'string' && jsonBody.trim() !== '') {
|
|
617
|
+
message = jsonBody.trim();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!message && textBody && textBody.trim() !== '') {
|
|
621
|
+
message = textBody.trim();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
statusMessage: message ?? fallbackMessage,
|
|
626
|
+
statusLabel,
|
|
627
|
+
refUpdate,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function toPartialRefUpdateFields(
|
|
632
|
+
branch?: string | null,
|
|
633
|
+
oldSha?: string | null,
|
|
634
|
+
newSha?: string | null,
|
|
635
|
+
): Partial<RefUpdate> | undefined {
|
|
636
|
+
const refUpdate: Partial<RefUpdate> = {};
|
|
637
|
+
|
|
638
|
+
if (typeof branch === 'string' && branch.trim() !== '') {
|
|
639
|
+
refUpdate.branch = branch.trim();
|
|
640
|
+
}
|
|
641
|
+
if (typeof oldSha === 'string' && oldSha.trim() !== '') {
|
|
642
|
+
refUpdate.oldSha = oldSha.trim();
|
|
643
|
+
}
|
|
644
|
+
if (typeof newSha === 'string' && newSha.trim() !== '') {
|
|
645
|
+
refUpdate.newSha = newSha.trim();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return Object.keys(refUpdate).length > 0 ? refUpdate : undefined;
|
|
496
649
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RefUpdate, RefUpdateReason } from './types';
|
|
2
|
+
|
|
3
|
+
export interface RefUpdateErrorOptions {
|
|
4
|
+
status: string;
|
|
5
|
+
message?: string;
|
|
6
|
+
refUpdate?: Partial<RefUpdate>;
|
|
7
|
+
reason?: RefUpdateReason;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RefUpdateError extends Error {
|
|
11
|
+
public readonly status: string;
|
|
12
|
+
public readonly reason: RefUpdateReason;
|
|
13
|
+
public readonly refUpdate?: Partial<RefUpdate>;
|
|
14
|
+
|
|
15
|
+
constructor(message: string, options: RefUpdateErrorOptions) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'RefUpdateError';
|
|
18
|
+
this.status = options.status;
|
|
19
|
+
this.reason = options.reason ?? inferRefUpdateReason(options.status);
|
|
20
|
+
this.refUpdate = options.refUpdate;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const REF_REASON_MAP: Record<string, RefUpdateReason> = {
|
|
25
|
+
precondition_failed: 'precondition_failed',
|
|
26
|
+
conflict: 'conflict',
|
|
27
|
+
not_found: 'not_found',
|
|
28
|
+
invalid: 'invalid',
|
|
29
|
+
timeout: 'timeout',
|
|
30
|
+
unauthorized: 'unauthorized',
|
|
31
|
+
forbidden: 'forbidden',
|
|
32
|
+
unavailable: 'unavailable',
|
|
33
|
+
internal: 'internal',
|
|
34
|
+
failed: 'failed',
|
|
35
|
+
ok: 'unknown',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function inferRefUpdateReason(status?: string): RefUpdateReason {
|
|
39
|
+
if (!status) {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const trimmed = status.trim();
|
|
44
|
+
if (trimmed === '') {
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const label = trimmed.toLowerCase();
|
|
49
|
+
return REF_REASON_MAP[label] ?? 'unknown';
|
|
50
|
+
}
|
package/src/fetch.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
|
+
import { errorEnvelopeSchema } from './schemas';
|
|
1
2
|
import type { ValidAPIVersion, ValidMethod, ValidPath } from './types';
|
|
2
3
|
|
|
3
4
|
interface RequestOptions {
|
|
4
5
|
allowedStatus?: number[];
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
export class ApiError extends Error {
|
|
9
|
+
public readonly status: number;
|
|
10
|
+
public readonly statusText: string;
|
|
11
|
+
public readonly method: ValidMethod;
|
|
12
|
+
public readonly url: string;
|
|
13
|
+
public readonly body?: unknown;
|
|
14
|
+
|
|
15
|
+
constructor(params: {
|
|
16
|
+
message: string;
|
|
17
|
+
status: number;
|
|
18
|
+
statusText: string;
|
|
19
|
+
method: ValidMethod;
|
|
20
|
+
url: string;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
}) {
|
|
23
|
+
super(params.message);
|
|
24
|
+
this.name = 'ApiError';
|
|
25
|
+
this.status = params.status;
|
|
26
|
+
this.statusText = params.statusText;
|
|
27
|
+
this.method = params.method;
|
|
28
|
+
this.url = params.url;
|
|
29
|
+
this.body = params.body;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
export class ApiFetcher {
|
|
8
34
|
constructor(
|
|
9
35
|
private readonly API_BASE_URL: string,
|
|
10
36
|
private readonly version: ValidAPIVersion,
|
|
11
|
-
) {
|
|
12
|
-
console.log('api fetcher created', API_BASE_URL, version);
|
|
13
|
-
}
|
|
37
|
+
) {}
|
|
14
38
|
|
|
15
39
|
private getBaseUrl() {
|
|
16
40
|
return `${this.API_BASE_URL}/api/v${this.version}`;
|
|
@@ -46,9 +70,55 @@ export class ApiFetcher {
|
|
|
46
70
|
|
|
47
71
|
if (!response.ok) {
|
|
48
72
|
const allowed = options?.allowedStatus ?? [];
|
|
49
|
-
if (
|
|
50
|
-
|
|
73
|
+
if (allowed.includes(response.status)) {
|
|
74
|
+
return response;
|
|
51
75
|
}
|
|
76
|
+
|
|
77
|
+
let errorBody: unknown;
|
|
78
|
+
let message: string | undefined;
|
|
79
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
if (contentType.includes('application/json')) {
|
|
83
|
+
errorBody = await response.json();
|
|
84
|
+
} else {
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
errorBody = text;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Fallback to plain text if JSON parse failed after reading body
|
|
90
|
+
try {
|
|
91
|
+
errorBody = await response.text();
|
|
92
|
+
} catch {
|
|
93
|
+
errorBody = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof errorBody === 'string') {
|
|
98
|
+
const trimmed = errorBody.trim();
|
|
99
|
+
if (trimmed) {
|
|
100
|
+
message = trimmed;
|
|
101
|
+
}
|
|
102
|
+
} else if (errorBody && typeof errorBody === 'object') {
|
|
103
|
+
const parsedError = errorEnvelopeSchema.safeParse(errorBody);
|
|
104
|
+
if (parsedError.success) {
|
|
105
|
+
const trimmed = parsedError.data.error.trim();
|
|
106
|
+
if (trimmed) {
|
|
107
|
+
message = trimmed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new ApiError({
|
|
113
|
+
message:
|
|
114
|
+
message ??
|
|
115
|
+
`Request ${method} ${requestUrl} failed with status ${response.status} ${response.statusText}`,
|
|
116
|
+
status: response.status,
|
|
117
|
+
statusText: response.statusText,
|
|
118
|
+
method,
|
|
119
|
+
url: requestUrl,
|
|
120
|
+
body: errorBody,
|
|
121
|
+
});
|
|
52
122
|
}
|
|
53
123
|
return response;
|
|
54
124
|
}
|