@atcute/util-fetch 1.0.0
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/LICENSE +17 -0
- package/README.md +3 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline.d.ts +7 -0
- package/dist/pipeline.js +7 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/streams/size-limit.d.ts +3 -0
- package/dist/streams/size-limit.js +17 -0
- package/dist/streams/size-limit.js.map +1 -0
- package/dist/transformers.d.ts +15 -0
- package/dist/transformers.js +69 -0
- package/dist/transformers.js.map +1 -0
- package/lib/errors.ts +45 -0
- package/lib/index.ts +3 -0
- package/lib/pipeline.ts +41 -0
- package/lib/streams/size-limit.ts +23 -0
- package/lib/transformers.ts +105 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
3
|
+
in the Software without restriction, including without limitation the rights
|
|
4
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
5
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
6
|
+
furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all
|
|
9
|
+
copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
17
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class FetchResponseError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
}
|
|
4
|
+
export declare class FailedResponseError extends FetchResponseError {
|
|
5
|
+
status: number;
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(status: number, reason: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class ImproperContentTypeError extends FetchResponseError {
|
|
10
|
+
contentType: string | null;
|
|
11
|
+
name: string;
|
|
12
|
+
constructor(contentType: string | null, reason: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class ImproperContentLengthError extends FetchResponseError {
|
|
15
|
+
expectedSize: number;
|
|
16
|
+
actualSize: number | null;
|
|
17
|
+
name: string;
|
|
18
|
+
constructor(expectedSize: number, actualSize: number | null, reason: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class ImproperResponseError extends FetchResponseError {
|
|
21
|
+
name: string;
|
|
22
|
+
constructor(reason: string, options?: ErrorOptions);
|
|
23
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class FetchResponseError extends Error {
|
|
2
|
+
name = 'FetchResponseError';
|
|
3
|
+
}
|
|
4
|
+
export class FailedResponseError extends FetchResponseError {
|
|
5
|
+
status;
|
|
6
|
+
name = 'FailedResponseError';
|
|
7
|
+
constructor(status, reason) {
|
|
8
|
+
super(reason);
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class ImproperContentTypeError extends FetchResponseError {
|
|
13
|
+
contentType;
|
|
14
|
+
name = 'ImproperContentTypeError';
|
|
15
|
+
constructor(contentType, reason) {
|
|
16
|
+
super(reason);
|
|
17
|
+
this.contentType = contentType;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class ImproperContentLengthError extends FetchResponseError {
|
|
21
|
+
expectedSize;
|
|
22
|
+
actualSize;
|
|
23
|
+
name = 'ImproperContentLengthError';
|
|
24
|
+
constructor(expectedSize, actualSize, reason) {
|
|
25
|
+
super(reason);
|
|
26
|
+
this.expectedSize = expectedSize;
|
|
27
|
+
this.actualSize = actualSize;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ImproperResponseError extends FetchResponseError {
|
|
31
|
+
name = 'ImproperResponseError';
|
|
32
|
+
constructor(reason, options) {
|
|
33
|
+
super(reason, options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../lib/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACnC,IAAI,GAAG,oBAAoB,CAAC;CACrC;AAED,MAAM,OAAO,mBAAoB,SAAQ,kBAAkB;IAIlD;IAHC,IAAI,GAAG,qBAAqB,CAAC;IAEtC,YACQ,MAAc,EACrB,MAAc;QAEd,KAAK,CAAC,MAAM,CAAC,CAAC;QAHP,WAAM,GAAN,MAAM,CAAQ;IAItB,CAAC;CACD;AAED,MAAM,OAAO,wBAAyB,SAAQ,kBAAkB;IAIvD;IAHC,IAAI,GAAG,0BAA0B,CAAC;IAE3C,YACQ,WAA0B,EACjC,MAAc;QAEd,KAAK,CAAC,MAAM,CAAC,CAAC;QAHP,gBAAW,GAAX,WAAW,CAAe;IAIlC,CAAC;CACD;AAED,MAAM,OAAO,0BAA2B,SAAQ,kBAAkB;IAIzD;IACA;IAJC,IAAI,GAAG,4BAA4B,CAAC;IAE7C,YACQ,YAAoB,EACpB,UAAyB,EAChC,MAAc;QAEd,KAAK,CAAC,MAAM,CAAC,CAAC;QAJP,iBAAY,GAAZ,YAAY,CAAQ;QACpB,eAAU,GAAV,UAAU,CAAe;IAIjC,CAAC;CACD;AAED,MAAM,OAAO,qBAAsB,SAAQ,kBAAkB;IACnD,IAAI,GAAG,uBAAuB,CAAC;IAExC,YAAY,MAAc,EAAE,OAAsB;QACjD,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxB,CAAC;CACD"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Transformer<I, O = I> = (input: I) => Promise<O>;
|
|
2
|
+
type PipelineInput<T extends readonly Transformer<any>[]> = T extends [Transformer<infer I, any>, ...any[]] ? I : T extends Transformer<infer I, any>[] ? I : never;
|
|
3
|
+
type PipelineOutput<T extends readonly Transformer<any>[]> = T extends [...any[], Transformer<any, infer O>] ? O : T extends Transformer<any, infer O>[] ? O : never;
|
|
4
|
+
type Pipeline<F extends readonly Transformer<any>[], Acc extends readonly Transformer<any>[] = []> = F extends [Transformer<infer I, infer O>] ? [...Acc, Transformer<I, O>] : F extends [Transformer<infer A, any>, ...infer Tail] ? Tail extends [Transformer<infer B, any>, ...any[]] ? Pipeline<Tail, [...Acc, Transformer<A, B>]> : Acc : Acc;
|
|
5
|
+
export declare function pipe(): never;
|
|
6
|
+
export declare function pipe<T extends readonly Transformer<any>[]>(...pipeline: Pipeline<T> extends T ? T : Pipeline<T>): (input: PipelineInput<T>) => Promise<PipelineOutput<T>>;
|
|
7
|
+
export {};
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../lib/pipeline.ts"],"names":[],"mappings":"AA6BA,MAAM,UAAU,IAAI,CACnB,GAAG,QAAiD;IAEpD,OAAO,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,OAAO,GAAG,CACf,KAAwB,EACxB,MAAyB,EACI,EAAE;IAC/B,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as err from '../errors.js';
|
|
2
|
+
export class SizeLimitStream extends TransformStream {
|
|
3
|
+
constructor(maxSize) {
|
|
4
|
+
let bytesRead = 0;
|
|
5
|
+
super({
|
|
6
|
+
transform(chunk, controller) {
|
|
7
|
+
bytesRead += chunk.length;
|
|
8
|
+
if (bytesRead > maxSize) {
|
|
9
|
+
controller.error(new err.ImproperContentLengthError(maxSize, bytesRead, `response content-length too large`));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
controller.enqueue(chunk);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=size-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-limit.js","sourceRoot":"","sources":["../../lib/streams/size-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,cAAc,CAAC;AAEpC,MAAM,OAAO,eAAgB,SAAQ,eAAuC;IAC3E,YAAY,OAAe;QAC1B,IAAI,SAAS,GAAG,CAAC,CAAC;QAElB,KAAK,CAAC;YACL,SAAS,CAAC,KAAK,EAAE,UAAU;gBAC1B,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC;gBAE1B,IAAI,SAAS,GAAG,OAAO,EAAE,CAAC;oBACzB,UAAU,CAAC,KAAK,CACf,IAAI,GAAG,CAAC,0BAA0B,CAAC,OAAO,EAAE,SAAS,EAAE,mCAAmC,CAAC,CAC3F,CAAC;oBAEF,OAAO;gBACR,CAAC;gBAED,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;SACD,CAAC,CAAC;IACJ,CAAC;CACD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
export type TextResponse = {
|
|
3
|
+
response: Response;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
export type ParsedJsonResponse<T = unknown> = {
|
|
7
|
+
response: Response;
|
|
8
|
+
json: T;
|
|
9
|
+
};
|
|
10
|
+
export declare const isResponseOk: (response: Response) => Promise<Response>;
|
|
11
|
+
export declare const readResponseAsText: (maxSize: number) => (response: Response) => Promise<TextResponse>;
|
|
12
|
+
export declare const parseResponseAsJson: (typeRegex: RegExp, maxSize: number) => (response: Response) => Promise<ParsedJsonResponse>;
|
|
13
|
+
type ParseOptions = NonNullable<Parameters<v.Type['parse']>[1]>;
|
|
14
|
+
export declare const validateJsonWith: <T>(schema: v.Type<T>, options?: ParseOptions) => (parsed: ParsedJsonResponse) => Promise<ParsedJsonResponse<T>>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
import * as err from './errors.js';
|
|
3
|
+
import { SizeLimitStream } from './streams/size-limit.js';
|
|
4
|
+
export const isResponseOk = async (response) => {
|
|
5
|
+
if (response.ok) {
|
|
6
|
+
return response;
|
|
7
|
+
}
|
|
8
|
+
if (response.body) {
|
|
9
|
+
await response.body.cancel();
|
|
10
|
+
}
|
|
11
|
+
throw new err.FailedResponseError(response.status, `got http ${response.status}`);
|
|
12
|
+
};
|
|
13
|
+
export const readResponseAsText = (maxSize) => async (response) => {
|
|
14
|
+
const text = await readResponse(response, maxSize);
|
|
15
|
+
return { response, text };
|
|
16
|
+
};
|
|
17
|
+
export const parseResponseAsJson = (typeRegex, maxSize) => async (response) => {
|
|
18
|
+
assertContentType(response, typeRegex);
|
|
19
|
+
const text = await readResponse(response, maxSize);
|
|
20
|
+
try {
|
|
21
|
+
const json = JSON.parse(text);
|
|
22
|
+
return { response, json };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new err.ImproperResponseError(`unexpected json data`, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const validateJsonWith = (schema, options) => async (parsed) => {
|
|
29
|
+
const json = schema.parse(parsed.json, options);
|
|
30
|
+
return { response: parsed.response, json };
|
|
31
|
+
};
|
|
32
|
+
const assertContentType = async (response, typeRegex) => {
|
|
33
|
+
const type = response.headers.get('content-type')?.split(';', 1)[0].trim();
|
|
34
|
+
if (type === undefined) {
|
|
35
|
+
if (response.body) {
|
|
36
|
+
await response.body.cancel();
|
|
37
|
+
}
|
|
38
|
+
throw new err.ImproperContentTypeError(null, `missing response content-type`);
|
|
39
|
+
}
|
|
40
|
+
if (!typeRegex.test(type)) {
|
|
41
|
+
if (response.body) {
|
|
42
|
+
await response.body.cancel();
|
|
43
|
+
}
|
|
44
|
+
throw new err.ImproperContentTypeError(type, `unexpected response content-type`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const readResponse = async (response, maxSize) => {
|
|
48
|
+
const rawSize = response.headers.get('content-length');
|
|
49
|
+
if (rawSize !== null) {
|
|
50
|
+
const size = Number(rawSize);
|
|
51
|
+
if (!Number.isSafeInteger(size) || size <= 0) {
|
|
52
|
+
response.body?.cancel();
|
|
53
|
+
throw new err.ImproperContentLengthError(maxSize, null, `invalid response content-length`);
|
|
54
|
+
}
|
|
55
|
+
if (size > maxSize) {
|
|
56
|
+
response.body?.cancel();
|
|
57
|
+
throw new err.ImproperContentLengthError(maxSize, size, `response content-length too large`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const stream = response
|
|
61
|
+
.body.pipeThrough(new SizeLimitStream(maxSize))
|
|
62
|
+
.pipeThrough(new TextDecoderStream());
|
|
63
|
+
let text = '';
|
|
64
|
+
for await (const chunk of stream) {
|
|
65
|
+
text += chunk;
|
|
66
|
+
}
|
|
67
|
+
return text;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=transformers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformers.js","sourceRoot":"","sources":["../lib/transformers.ts"],"names":[],"mappings":"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"}
|
package/lib/errors.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class FetchResponseError extends Error {
|
|
2
|
+
override name = 'FetchResponseError';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class FailedResponseError extends FetchResponseError {
|
|
6
|
+
override name = 'FailedResponseError';
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
public status: number,
|
|
10
|
+
reason: string,
|
|
11
|
+
) {
|
|
12
|
+
super(reason);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ImproperContentTypeError extends FetchResponseError {
|
|
17
|
+
override name = 'ImproperContentTypeError';
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public contentType: string | null,
|
|
21
|
+
reason: string,
|
|
22
|
+
) {
|
|
23
|
+
super(reason);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ImproperContentLengthError extends FetchResponseError {
|
|
28
|
+
override name = 'ImproperContentLengthError';
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
public expectedSize: number,
|
|
32
|
+
public actualSize: number | null,
|
|
33
|
+
reason: string,
|
|
34
|
+
) {
|
|
35
|
+
super(reason);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ImproperResponseError extends FetchResponseError {
|
|
40
|
+
override name = 'ImproperResponseError';
|
|
41
|
+
|
|
42
|
+
constructor(reason: string, options?: ErrorOptions) {
|
|
43
|
+
super(reason, options);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/lib/index.ts
ADDED
package/lib/pipeline.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type Transformer<I, O = I> = (input: I) => Promise<O>;
|
|
2
|
+
|
|
3
|
+
type PipelineInput<T extends readonly Transformer<any>[]> = T extends [Transformer<infer I, any>, ...any[]]
|
|
4
|
+
? I
|
|
5
|
+
: T extends Transformer<infer I, any>[]
|
|
6
|
+
? I
|
|
7
|
+
: never;
|
|
8
|
+
|
|
9
|
+
type PipelineOutput<T extends readonly Transformer<any>[]> = T extends [...any[], Transformer<any, infer O>]
|
|
10
|
+
? O
|
|
11
|
+
: T extends Transformer<any, infer O>[]
|
|
12
|
+
? O
|
|
13
|
+
: never;
|
|
14
|
+
|
|
15
|
+
type Pipeline<
|
|
16
|
+
F extends readonly Transformer<any>[],
|
|
17
|
+
Acc extends readonly Transformer<any>[] = [],
|
|
18
|
+
> = F extends [Transformer<infer I, infer O>]
|
|
19
|
+
? [...Acc, Transformer<I, O>]
|
|
20
|
+
: F extends [Transformer<infer A, any>, ...infer Tail]
|
|
21
|
+
? Tail extends [Transformer<infer B, any>, ...any[]]
|
|
22
|
+
? Pipeline<Tail, [...Acc, Transformer<A, B>]>
|
|
23
|
+
: Acc
|
|
24
|
+
: Acc;
|
|
25
|
+
|
|
26
|
+
export function pipe(): never;
|
|
27
|
+
export function pipe<T extends readonly Transformer<any>[]>(
|
|
28
|
+
...pipeline: Pipeline<T> extends T ? T : Pipeline<T>
|
|
29
|
+
): (input: PipelineInput<T>) => Promise<PipelineOutput<T>>;
|
|
30
|
+
export function pipe<T extends readonly Transformer<any>[]>(
|
|
31
|
+
...pipeline: Pipeline<T> extends T ? T : Pipeline<T>
|
|
32
|
+
): (input: PipelineInput<T>) => Promise<PipelineOutput<T>> {
|
|
33
|
+
return pipeline.reduce(pipeTwo);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pipeTwo = <I, O, X = unknown>(
|
|
37
|
+
first: Transformer<I, X>,
|
|
38
|
+
second: Transformer<X, O>,
|
|
39
|
+
): ((input: I) => Promise<O>) => {
|
|
40
|
+
return (input) => first(input).then(second);
|
|
41
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as err from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export class SizeLimitStream extends TransformStream<Uint8Array, Uint8Array> {
|
|
4
|
+
constructor(maxSize: number) {
|
|
5
|
+
let bytesRead = 0;
|
|
6
|
+
|
|
7
|
+
super({
|
|
8
|
+
transform(chunk, controller) {
|
|
9
|
+
bytesRead += chunk.length;
|
|
10
|
+
|
|
11
|
+
if (bytesRead > maxSize) {
|
|
12
|
+
controller.error(
|
|
13
|
+
new err.ImproperContentLengthError(maxSize, bytesRead, `response content-length too large`),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
controller.enqueue(chunk);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import * as err from './errors.js';
|
|
4
|
+
import { SizeLimitStream } from './streams/size-limit.js';
|
|
5
|
+
|
|
6
|
+
export type TextResponse = {
|
|
7
|
+
response: Response;
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ParsedJsonResponse<T = unknown> = {
|
|
12
|
+
response: Response;
|
|
13
|
+
json: T;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const isResponseOk = async (response: Response): Promise<Response> => {
|
|
17
|
+
if (response.ok) {
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (response.body) {
|
|
22
|
+
await response.body.cancel();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new err.FailedResponseError(response.status, `got http ${response.status}`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const readResponseAsText =
|
|
29
|
+
(maxSize: number) =>
|
|
30
|
+
async (response: Response): Promise<TextResponse> => {
|
|
31
|
+
const text = await readResponse(response, maxSize);
|
|
32
|
+
return { response, text };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const parseResponseAsJson =
|
|
36
|
+
(typeRegex: RegExp, maxSize: number) =>
|
|
37
|
+
async (response: Response): Promise<ParsedJsonResponse> => {
|
|
38
|
+
assertContentType(response, typeRegex);
|
|
39
|
+
|
|
40
|
+
const text = await readResponse(response, maxSize);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const json = JSON.parse(text);
|
|
44
|
+
return { response, json };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new err.ImproperResponseError(`unexpected json data`, { cause: error });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ParseOptions = NonNullable<Parameters<v.Type['parse']>[1]>;
|
|
51
|
+
|
|
52
|
+
export const validateJsonWith =
|
|
53
|
+
<T>(schema: v.Type<T>, options?: ParseOptions) =>
|
|
54
|
+
async (parsed: ParsedJsonResponse): Promise<ParsedJsonResponse<T>> => {
|
|
55
|
+
const json = schema.parse(parsed.json, options);
|
|
56
|
+
return { response: parsed.response, json };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const assertContentType = async (response: Response, typeRegex: RegExp): Promise<void> => {
|
|
60
|
+
const type = response.headers.get('content-type')?.split(';', 1)[0].trim();
|
|
61
|
+
|
|
62
|
+
if (type === undefined) {
|
|
63
|
+
if (response.body) {
|
|
64
|
+
await response.body.cancel();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new err.ImproperContentTypeError(null, `missing response content-type`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!typeRegex.test(type)) {
|
|
71
|
+
if (response.body) {
|
|
72
|
+
await response.body.cancel();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new err.ImproperContentTypeError(type, `unexpected response content-type`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const readResponse = async (response: Response, maxSize: number): Promise<string> => {
|
|
80
|
+
const rawSize = response.headers.get('content-length');
|
|
81
|
+
if (rawSize !== null) {
|
|
82
|
+
const size = Number(rawSize);
|
|
83
|
+
|
|
84
|
+
if (!Number.isSafeInteger(size) || size <= 0) {
|
|
85
|
+
response.body?.cancel();
|
|
86
|
+
throw new err.ImproperContentLengthError(maxSize, null, `invalid response content-length`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (size > maxSize) {
|
|
90
|
+
response.body?.cancel();
|
|
91
|
+
throw new err.ImproperContentLengthError(maxSize, size, `response content-length too large`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stream = response
|
|
96
|
+
.body!.pipeThrough(new SizeLimitStream(maxSize))
|
|
97
|
+
.pipeThrough(new TextDecoderStream());
|
|
98
|
+
|
|
99
|
+
let text = '';
|
|
100
|
+
for await (const chunk of stream) {
|
|
101
|
+
text += chunk;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return text;
|
|
105
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@atcute/util-fetch",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "atproto DID to DID document resolution",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto",
|
|
8
|
+
"did"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"url": "https://github.com/mary-ext/atcute",
|
|
13
|
+
"directory": "packages/misc/util-fetch"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"lib/",
|
|
18
|
+
"!lib/**/*.bench.ts",
|
|
19
|
+
"!lib/**/*.test.ts"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bun": "^1.2.1"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@badrap/valita": "^0.4.2"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc --project tsconfig.build.json",
|
|
33
|
+
"test": "bun test --coverage",
|
|
34
|
+
"prepublish": "rm -rf dist; pnpm run build"
|
|
35
|
+
}
|
|
36
|
+
}
|