@atproto/lex-client 0.0.13 → 0.0.15
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/CHANGELOG.md +21 -0
- package/dist/agent.d.ts +24 -19
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +11 -33
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +11 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +35 -7
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +94 -19
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +1 -1
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +2 -2
- package/dist/response.js.map +1 -1
- package/dist/xrpc.d.ts +14 -8
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +5 -3
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/agent.ts +38 -21
- package/src/client.ts +16 -8
- package/src/errors.test.ts +346 -0
- package/src/errors.ts +120 -19
- package/src/response.ts +4 -4
- package/src/xrpc.ts +22 -14
package/dist/xrpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xrpc.js","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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","sourcesContent":["import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'\nimport { lexStringify } from '@atproto/lex-json'\nimport {\n InferInput,\n InferPayload,\n Main,\n Params,\n Payload,\n Procedure,\n Query,\n Restricted,\n Subscription,\n getMain,\n} from '@atproto/lex-schema'\nimport { Agent } from './agent.js'\nimport { XrpcFailure, asXrpcFailure } from './errors.js'\nimport { XrpcResponse } from './response.js'\nimport { BinaryBodyInit, CallOptions } from './types.js'\nimport {\n buildAtprotoHeaders,\n isAsyncIterable,\n isBlobLike,\n toReadableStream,\n} from './util.js'\n\n// If all params are optional, allow omitting the params object\ntype XrpcParamsOptions<P extends Params> =\n NonNullable<unknown> extends P ? { params?: P } : { params: P }\n\n/**\n * The query/path parameters type for an XRPC method, inferred from its schema.\n *\n * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)\n */\nexport type XrpcRequestParams<M extends Procedure | Query | Subscription> =\n InferInput<M['parameters']>\n\ntype XrpcRequestPayload<M extends Procedure | Query> = M extends Procedure\n ? InferPayload<M['input'], BinaryBodyInit>\n : undefined\n\ntype XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }\n ? // encoding will be inferred from the schema at runtime if not provided\n { body: B; encoding?: E }\n : { body?: undefined; encoding?: undefined }\n\n/**\n * Options for making an XRPC request.\n *\n * Combines {@link CallOptions} with method-specific params and body requirements.\n * The type system ensures required params/body are provided based on the method schema.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n * @see {@link CallOptions} for general request options like signal and validateRequest\n * @see {@link XrpcParamsOptions} for method-specific query parameters\n * @see {@link XrpcInputOptions} for method-specific body and encoding requirements\n *\n * @example Query with params\n * ```typescript\n * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {\n * params: { limit: 50 }\n * }\n * ```\n *\n * @example Procedure with body\n * ```typescript\n * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {\n * body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }\n * }\n * ```\n */\nexport type XrpcOptions<M extends Procedure | Query = Procedure | Query> =\n CallOptions &\n XrpcInputOptions<XrpcRequestPayload<M>> &\n XrpcParamsOptions<XrpcRequestParams<M>>\n\n/**\n * Makes an XRPC request and throws on failure.\n *\n * This is the low-level function for making XRPC calls. For most use cases,\n * prefer using {@link Client.xrpc} which provides a more ergonomic API.\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns The successful {@link XrpcResponse}\n * @throws {XrpcFailure} When the request fails\n *\n * @example\n * ```typescript\n * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {\n * params: { limit: 50 }\n * })\n * ```\n */\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResponse<M>> {\n const response = await xrpcSafe<M>(agent, ns, options)\n if (response.success) return response\n else throw response\n}\n\n/**\n * Union type representing either a successful response or a failure.\n *\n * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property\n * that can be used to discriminate between them.\n *\n * @typeParam M - The XRPC method type\n */\nexport type XrpcResult<M extends Procedure | Query> =\n | XrpcResponse<M>\n | XrpcFailure<M>\n\n/**\n * Makes an XRPC request without throwing on failure.\n *\n * Returns a discriminated union that can be checked via the `success` property.\n * This is useful for handling errors without try/catch blocks. This also allow\n * failure results to be typed with the method schema, which can provide better\n * type safety when handling errors (e.g. checking for specific error codes).\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}\n *\n * @example\n * ```typescript\n * const result = await xrpcSafe(agent, app.bsky.actor.getProfile.main, {\n * params: { actor: 'alice.bsky.social' }\n * })\n *\n * if (result.success) {\n * console.log(result.body.displayName)\n * } else {\n * console.error('Request failed:', result.error)\n * }\n * ```\n */\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agent: Agent,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agent: Agent,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResult<M>> {\n options.signal?.throwIfAborted()\n const method: M = getMain(ns)\n try {\n const url = xrpcRequestUrl(method, options)\n const request = xrpcRequestInit(method, options)\n const response = await agent.fetchHandler(url, request)\n return await XrpcResponse.fromFetchResponse<M>(method, response, options)\n } catch (cause) {\n return asXrpcFailure(method, cause)\n }\n}\n\nfunction xrpcRequestUrl<M extends Procedure | Query | Subscription>(\n method: M,\n options: CallOptions & { params?: Params },\n) {\n const path = `/xrpc/${method.nsid}`\n\n const queryString = method.parameters\n ?.toURLSearchParams(options.params ?? {})\n .toString()\n\n return queryString ? `${path}?${queryString}` : path\n}\n\nfunction xrpcRequestInit<T extends Procedure | Query>(\n schema: T,\n options: CallOptions & {\n body?: LexValue | BinaryBodyInit\n encoding?: string\n },\n): RequestInit & { duplex?: 'half' } {\n const headers = buildAtprotoHeaders(options)\n\n // Tell the server what type of response we're expecting\n if (schema.output.encoding) {\n headers.set('accept', schema.output.encoding)\n }\n\n // Caller should not set content-type header\n if (headers.has('content-type')) {\n const contentType = headers.get('content-type')\n throw new TypeError(`Unexpected content-type header (${contentType})`)\n }\n\n // Requests with body\n if ('input' in schema) {\n const encodingHint = options.encoding\n const input = xrpcProcedureInput(schema, options, encodingHint)\n\n if (input) {\n headers.set('content-type', input.encoding)\n } else if (encodingHint != null) {\n throw new TypeError(`Unexpected encoding hint (${encodingHint})`)\n }\n\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'POST',\n headers,\n body: input?.body,\n }\n }\n\n // Requests without body\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'GET',\n headers,\n }\n}\n\nfunction xrpcProcedureInput(\n method: Procedure,\n options: CallOptions & { body?: LexValue | BinaryBodyInit },\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n const { input } = method\n const { body } = options\n\n if (options.validateRequest) {\n input.schema?.check(body)\n }\n\n // Special handling for endpoints expecting application/json input\n if (input.encoding === 'application/json') {\n // @NOTE **NOT** using isLexValue here to avoid deep checks in order to\n // distinguish between LexValue and BinaryBodyInit.\n if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {\n throw new TypeError(`Expected LexValue body, got ${typeof body}`)\n }\n\n return buildPayload(input, lexStringify(body), encodingHint)\n }\n\n // Other encodings will be sent unaltered (ie. as binary data)\n switch (typeof body) {\n case 'undefined':\n case 'string':\n return buildPayload(input, body, encodingHint)\n case 'object': {\n if (body === null) break\n if (\n ArrayBuffer.isView(body) ||\n body instanceof ArrayBuffer ||\n body instanceof ReadableStream\n ) {\n return buildPayload(input, body, encodingHint)\n } else if (isAsyncIterable(body)) {\n return buildPayload(input, toReadableStream(body), encodingHint)\n } else if (isBlobLike(body)) {\n return buildPayload(input, body, encodingHint || body.type)\n }\n }\n }\n\n throw new TypeError(\n `Invalid ${typeof body} body for ${input.encoding} encoding`,\n )\n}\n\nfunction buildPayload(\n schema: Payload,\n body: undefined | BodyInit,\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n if (schema.encoding === undefined) {\n if (body !== undefined) {\n throw new TypeError(\n `Cannot send a ${typeof body} body with undefined encoding`,\n )\n }\n\n return null\n }\n\n if (body === undefined) {\n // This error would be returned by the server, but we can catch it earlier\n // to avoid un-necessary requests. Note that a content-length of 0 does not\n // necessary mean that the body is \"empty\" (e.g. an empty txt file).\n throw new TypeError(`A request body is expected but none was provided`)\n }\n\n const encoding = buildEncoding(schema, encodingHint)\n return { encoding, body }\n}\n\nfunction buildEncoding(schema: Payload, encodingHint?: string): string {\n // Should never happen (required for type safety)\n if (!schema.encoding) {\n throw new TypeError('Unexpected payload')\n }\n\n if (encodingHint?.length) {\n if (!schema.matchesEncoding(encodingHint)) {\n throw new TypeError(\n `Cannot send a body with content-type \"${encodingHint}\" for \"${schema.encoding}\" encoding`,\n )\n }\n return encodingHint\n }\n\n // Fallback\n\n if (schema.encoding === '*/*') {\n return 'application/octet-stream'\n }\n\n if (schema.encoding.startsWith('text/')) {\n return schema.encoding.includes('*')\n ? 'text/plain; charset=utf-8'\n : `${schema.encoding}; charset=utf-8`\n }\n\n if (!schema.encoding.includes('*')) {\n return schema.encoding\n }\n\n throw new TypeError(\n `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"xrpc.js","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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","sourcesContent":["import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'\nimport { lexStringify } from '@atproto/lex-json'\nimport {\n InferInput,\n InferPayload,\n Main,\n NsidString,\n Params,\n Payload,\n Procedure,\n Query,\n Restricted,\n Subscription,\n getMain,\n} from '@atproto/lex-schema'\nimport { Agent, AgentOptions, buildAgent } from './agent.js'\nimport { XrpcFailure, asXrpcFailure } from './errors.js'\nimport { XrpcResponse } from './response.js'\nimport { BinaryBodyInit, CallOptions } from './types.js'\nimport {\n buildAtprotoHeaders,\n isAsyncIterable,\n isBlobLike,\n toReadableStream,\n} from './util.js'\n\n// If all params are optional, allow omitting the params object\ntype XrpcParamsOptions<P extends Params> =\n NonNullable<unknown> extends P ? { params?: P } : { params: P }\n\n/**\n * The query/path parameters type for an XRPC method, inferred from its schema.\n *\n * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)\n */\nexport type XrpcRequestParams<M extends Procedure | Query | Subscription> =\n InferInput<M['parameters']>\n\ntype XrpcRequestPayload<M extends Procedure | Query> = M extends Procedure\n ? InferPayload<M['input'], BinaryBodyInit>\n : undefined\n\ntype XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }\n ? // encoding will be inferred from the schema at runtime if not provided\n { body: B; encoding?: E }\n : { body?: undefined; encoding?: undefined }\n\n/**\n * Options for making an XRPC request.\n *\n * Combines {@link CallOptions} with method-specific params and body requirements.\n * The type system ensures required params/body are provided based on the method schema.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n * @see {@link CallOptions} for general request options like signal and validateRequest\n * @see {@link XrpcParamsOptions} for method-specific query parameters\n * @see {@link XrpcInputOptions} for method-specific body and encoding requirements\n *\n * @example Query with params\n * ```typescript\n * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {\n * params: { limit: 50 }\n * }\n * ```\n *\n * @example Procedure with body\n * ```typescript\n * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {\n * body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }\n * }\n * ```\n */\nexport type XrpcOptions<M extends Procedure | Query = Procedure | Query> =\n CallOptions &\n XrpcInputOptions<XrpcRequestPayload<M>> &\n XrpcParamsOptions<XrpcRequestParams<M>>\n\n/**\n * Makes an XRPC request and throws on failure.\n *\n * This is the low-level function for making XRPC calls.\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns The successful {@link XrpcResponse}\n * @throws {XrpcFailure} When the request fails\n *\n * @example\n * ```typescript\n * const response = await xrpc('https://bsky.network', com.atproto.identity.resolveHandle, {\n * params: { handle: \"atproto.com\" }\n * })\n * ```\n *\n * @example\n * ```typescript\n * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {\n * params: { limit: 50 }\n * })\n * ```\n */\nexport async function xrpc<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResponse<M>> {\n const response = await xrpcSafe<M>(agentOpts, ns, options)\n if (response.success) return response\n else throw response\n}\n\n/**\n * Union type representing either a successful response or a failure.\n *\n * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property\n * that can be used to discriminate between them.\n *\n * @typeParam M - The XRPC method type\n */\nexport type XrpcResult<M extends Procedure | Query> =\n | XrpcResponse<M>\n | XrpcFailure<M>\n\n/**\n * Makes an XRPC request without throwing on failure.\n *\n * Returns a discriminated union that can be checked via the `success` property.\n * This is useful for handling errors without try/catch blocks. This also allow\n * failure results to be typed with the method schema, which can provide better\n * type safety when handling errors (e.g. checking for specific error codes).\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}\n *\n * @example\n * ```typescript\n * const result = await xrpcSafe('https://example.com', app.bsky.actor.getProfile, {\n * params: { actor: 'alice.bsky.social' }\n * })\n *\n * if (result.success) {\n * console.log(result.body.displayName)\n * } else {\n * console.error('Request failed:', result.error)\n * }\n * ```\n */\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Main<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n agentOpts: Agent | AgentOptions,\n ns: Main<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResult<M>> {\n options.signal?.throwIfAborted()\n const method: M = getMain(ns)\n try {\n const agent = buildAgent(agentOpts)\n const url = xrpcRequestUrl(method, options)\n const request = xrpcRequestInit(method, options)\n const response = await agent.fetchHandler(url, request)\n return await XrpcResponse.fromFetchResponse<M>(method, response, options)\n } catch (cause) {\n return asXrpcFailure(method, cause)\n }\n}\n\nfunction xrpcRequestUrl<M extends Procedure | Query | Subscription>(\n method: M,\n options: CallOptions & { params?: Params },\n): `/xrpc/${NsidString}${'' | `?${string}`}` {\n const path = `/xrpc/${method.nsid}` as const\n\n const queryString = method.parameters\n ?.toURLSearchParams(options.params ?? {})\n .toString()\n\n return queryString ? (`${path}?${queryString}` as const) : path\n}\n\nfunction xrpcRequestInit<T extends Procedure | Query>(\n schema: T,\n options: CallOptions & {\n body?: LexValue | BinaryBodyInit\n encoding?: string\n },\n): RequestInit & { duplex?: 'half' } {\n const headers = buildAtprotoHeaders(options)\n\n // Tell the server what type of response we're expecting\n if (schema.output.encoding) {\n headers.set('accept', schema.output.encoding)\n }\n\n // Caller should not set content-type header\n if (headers.has('content-type')) {\n const contentType = headers.get('content-type')\n throw new TypeError(`Unexpected content-type header (${contentType})`)\n }\n\n // Requests with body\n if ('input' in schema) {\n const encodingHint = options.encoding\n const input = xrpcProcedureInput(schema, options, encodingHint)\n\n if (input) {\n headers.set('content-type', input.encoding)\n } else if (encodingHint != null) {\n throw new TypeError(`Unexpected encoding hint (${encodingHint})`)\n }\n\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'POST',\n headers,\n body: input?.body,\n }\n }\n\n // Requests without body\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'GET',\n headers,\n }\n}\n\nfunction xrpcProcedureInput(\n method: Procedure,\n options: CallOptions & { body?: LexValue | BinaryBodyInit },\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n const { input } = method\n const { body } = options\n\n if (options.validateRequest) {\n input.schema?.check(body)\n }\n\n // Special handling for endpoints expecting application/json input\n if (input.encoding === 'application/json') {\n // @NOTE **NOT** using isLexValue here to avoid deep checks in order to\n // distinguish between LexValue and BinaryBodyInit.\n if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {\n throw new TypeError(`Expected LexValue body, got ${typeof body}`)\n }\n\n return buildPayload(input, lexStringify(body), encodingHint)\n }\n\n // Other encodings will be sent unaltered (ie. as binary data)\n switch (typeof body) {\n case 'undefined':\n case 'string':\n return buildPayload(input, body, encodingHint)\n case 'object': {\n if (body === null) break\n if (\n ArrayBuffer.isView(body) ||\n body instanceof ArrayBuffer ||\n body instanceof ReadableStream\n ) {\n return buildPayload(input, body, encodingHint)\n } else if (isAsyncIterable(body)) {\n return buildPayload(input, toReadableStream(body), encodingHint)\n } else if (isBlobLike(body)) {\n return buildPayload(input, body, encodingHint || body.type)\n }\n }\n }\n\n throw new TypeError(\n `Invalid ${typeof body} body for ${input.encoding} encoding`,\n )\n}\n\nfunction buildPayload(\n schema: Payload,\n body: undefined | BodyInit,\n encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n if (schema.encoding === undefined) {\n if (body !== undefined) {\n throw new TypeError(\n `Cannot send a ${typeof body} body with undefined encoding`,\n )\n }\n\n return null\n }\n\n if (body === undefined) {\n // This error would be returned by the server, but we can catch it earlier\n // to avoid un-necessary requests. Note that a content-length of 0 does not\n // necessary mean that the body is \"empty\" (e.g. an empty txt file).\n throw new TypeError(`A request body is expected but none was provided`)\n }\n\n const encoding = buildEncoding(schema, encodingHint)\n return { encoding, body }\n}\n\nfunction buildEncoding(schema: Payload, encodingHint?: string): string {\n // Should never happen (required for type safety)\n if (!schema.encoding) {\n throw new TypeError('Unexpected payload')\n }\n\n if (encodingHint?.length) {\n if (!schema.matchesEncoding(encodingHint)) {\n throw new TypeError(\n `Cannot send a body with content-type \"${encodingHint}\" for \"${schema.encoding}\" encoding`,\n )\n }\n return encodingHint\n }\n\n // Fallback\n\n if (schema.encoding === '*/*') {\n return 'application/octet-stream'\n }\n\n if (schema.encoding.startsWith('text/')) {\n return schema.encoding.includes('*')\n ? 'text/plain; charset=utf-8'\n : `${schema.encoding}; charset=utf-8`\n }\n\n if (!schema.encoding.includes('*')) {\n return schema.encoding\n }\n\n throw new TypeError(\n `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,\n )\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "HTTP client for interacting with Lexicon based APIs",
|
|
6
6
|
"keywords": [
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"tslib": "^2.8.1",
|
|
40
|
-
"@atproto/lex-data": "^0.0.
|
|
41
|
-
"@atproto/lex-json": "^0.0.
|
|
42
|
-
"@atproto/lex-schema": "^0.0.
|
|
40
|
+
"@atproto/lex-data": "^0.0.13",
|
|
41
|
+
"@atproto/lex-json": "^0.0.13",
|
|
42
|
+
"@atproto/lex-schema": "^0.0.14"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"vitest": "^4.0.16",
|
|
46
|
-
"@atproto/lex-cbor": "^0.0.
|
|
47
|
-
"@atproto/lex-builder": "^0.0.
|
|
46
|
+
"@atproto/lex-cbor": "^0.0.14",
|
|
47
|
+
"@atproto/lex-builder": "^0.0.17"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"prebuild": "node ./scripts/lex-build.mjs",
|
package/src/agent.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { DidString } from '@atproto/lex-schema'
|
|
|
5
5
|
*
|
|
6
6
|
* The handler is responsible for adding the origin (protocol, hostname, and
|
|
7
7
|
* port) to the provided path, typically based on authentication or service
|
|
8
|
-
* configuration. The handler
|
|
8
|
+
* configuration. The handler can also be responsible for adding any necessary
|
|
9
9
|
* headers, such as authorization tokens.
|
|
10
10
|
*
|
|
11
11
|
* @param path - The URL path (pathname + query parameters) without the origin
|
|
@@ -18,16 +18,23 @@ export type FetchHandler = (
|
|
|
18
18
|
* origin. The origin (protocol, hostname, and port) must be added by this
|
|
19
19
|
* {@link FetchHandler}, typically based on authentication or other factors.
|
|
20
20
|
*/
|
|
21
|
-
path: string
|
|
21
|
+
path: `/${string}`,
|
|
22
22
|
init: RequestInit,
|
|
23
23
|
) => Promise<Response>
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Core interface for making XRPC requests.
|
|
26
|
+
* Core interface for making XRPC requests towards a specific service.
|
|
27
27
|
*
|
|
28
|
-
* An Agent encapsulates an identity and request handling for AT
|
|
29
|
-
* operations.
|
|
30
|
-
*
|
|
28
|
+
* An {@link Agent} encapsulates an identity and request handling for AT
|
|
29
|
+
* Protocol operations. Agents will typically handle authentication, service URL
|
|
30
|
+
* resolution, and other request configuration, allowing client code to make
|
|
31
|
+
* requests without needing to manage these details directly. The key component
|
|
32
|
+
* of an Agent is the {@link FetchHandler}, which is responsible for
|
|
33
|
+
* constructing the full request URL and adding any necessary headers or
|
|
34
|
+
* authentication tokens. The Agent's `did` property represents the
|
|
35
|
+
* authenticated user's DID, if available, and can be used for operations that
|
|
36
|
+
* require knowledge of the user's identity (such as creating AT Protocol
|
|
37
|
+
* records).
|
|
31
38
|
*
|
|
32
39
|
* @see {@link buildAgent} for creating (simple) Agent instances.
|
|
33
40
|
*
|
|
@@ -35,7 +42,10 @@ export type FetchHandler = (
|
|
|
35
42
|
* ```typescript
|
|
36
43
|
* const agent: Agent = {
|
|
37
44
|
* did: 'did:plc:example123',
|
|
38
|
-
* fetchHandler: (path, init) =>
|
|
45
|
+
* fetchHandler: async (path, init) => {
|
|
46
|
+
* const url = new URL(path, 'https://bsky.social')
|
|
47
|
+
* return fetch(url, init)
|
|
48
|
+
* }
|
|
39
49
|
* }
|
|
40
50
|
* ```
|
|
41
51
|
*/
|
|
@@ -46,6 +56,18 @@ export interface Agent {
|
|
|
46
56
|
fetchHandler: FetchHandler
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
export function isAgent(value: unknown): value is Agent {
|
|
60
|
+
return (
|
|
61
|
+
typeof value === 'object' &&
|
|
62
|
+
value !== null &&
|
|
63
|
+
'fetchHandler' in value &&
|
|
64
|
+
typeof value.fetchHandler === 'function' &&
|
|
65
|
+
(!('did' in value) ||
|
|
66
|
+
value.did === undefined ||
|
|
67
|
+
typeof value.did === 'string')
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
export type AgentConfig = {
|
|
50
72
|
/**
|
|
51
73
|
* The identifier (DID) of the user represented by this agent.
|
|
@@ -84,20 +106,20 @@ export type AgentOptions = AgentConfig | string | URL
|
|
|
84
106
|
/**
|
|
85
107
|
* Creates an {@link Agent} from various input types.
|
|
86
108
|
*
|
|
87
|
-
* This factory function accepts an existing Agent (returned as-is), a service
|
|
88
|
-
* or a full configuration object. It handles the common case of creating
|
|
89
|
-
* unauthenticated agent from just a service URL.
|
|
109
|
+
* This factory function accepts an existing Agent (returned as-is), a service
|
|
110
|
+
* URL, or a full configuration object. It handles the common case of creating
|
|
111
|
+
* an unauthenticated agent from just a service URL.
|
|
90
112
|
*
|
|
91
113
|
* @param options - Agent instance, configuration object, or service URL
|
|
92
114
|
* @returns A configured Agent ready for making requests
|
|
93
115
|
* @throws {TypeError} If fetch() is not available in the environment
|
|
94
116
|
*
|
|
95
|
-
* @example From URL string
|
|
117
|
+
* @example // From URL string
|
|
96
118
|
* ```typescript
|
|
97
119
|
* const agent = buildAgent('https://public.api.bsky.app')
|
|
98
120
|
* ```
|
|
99
121
|
*
|
|
100
|
-
* @example From configuration
|
|
122
|
+
* @example // From configuration
|
|
101
123
|
* ```typescript
|
|
102
124
|
* const agent = buildAgent({
|
|
103
125
|
* did: 'did:plc:example',
|
|
@@ -105,17 +127,12 @@ export type AgentOptions = AgentConfig | string | URL
|
|
|
105
127
|
* headers: { 'Authorization': 'Bearer ...' }
|
|
106
128
|
* })
|
|
107
129
|
* ```
|
|
108
|
-
*
|
|
109
|
-
* @example Pass-through existing agent
|
|
110
|
-
* ```typescript
|
|
111
|
-
* const existing: Agent = { ... }
|
|
112
|
-
* const agent = buildAgent(existing) // Returns existing unchanged
|
|
113
|
-
* ```
|
|
114
130
|
*/
|
|
131
|
+
export function buildAgent<O extends Agent | AgentOptions>(
|
|
132
|
+
options: O,
|
|
133
|
+
): O extends Agent ? O : Agent
|
|
115
134
|
export function buildAgent(options: Agent | AgentOptions): Agent {
|
|
116
|
-
if (
|
|
117
|
-
return options
|
|
118
|
-
}
|
|
135
|
+
if (isAgent(options)) return options
|
|
119
136
|
|
|
120
137
|
const config: Agent | AgentConfig =
|
|
121
138
|
typeof options === 'string' || options instanceof URL
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LexMap, LexValue, TypedLexMap } from '@atproto/lex-data'
|
|
2
2
|
import {
|
|
3
3
|
AtIdentifierString,
|
|
4
4
|
CidString,
|
|
@@ -282,11 +282,12 @@ export type ListRecord<Value extends LexMap> =
|
|
|
282
282
|
* services. It provides type-safe methods for XRPC calls, record operations,
|
|
283
283
|
* and blob handling.
|
|
284
284
|
*
|
|
285
|
-
* @example Basic usage
|
|
285
|
+
* @example // Basic usage
|
|
286
286
|
* ```typescript
|
|
287
287
|
* import { Client } from '@atproto/lex'
|
|
288
288
|
*
|
|
289
|
-
* const client = new Client(
|
|
289
|
+
* const client = new Client(oauthSession)
|
|
290
|
+
*
|
|
290
291
|
* const response = await client.xrpc(app.bsky.feed.getTimeline.main, {
|
|
291
292
|
* params: { limit: 50 }
|
|
292
293
|
* })
|
|
@@ -330,7 +331,7 @@ export class Client implements Agent {
|
|
|
330
331
|
|
|
331
332
|
/**
|
|
332
333
|
* The DID of the authenticated user.
|
|
333
|
-
* @throws {
|
|
334
|
+
* @throws {Error} if not authenticated
|
|
334
335
|
*/
|
|
335
336
|
get assertDid(): DidString {
|
|
336
337
|
this.assertAuthenticated()
|
|
@@ -341,7 +342,7 @@ export class Client implements Agent {
|
|
|
341
342
|
* Asserts that the client is authenticated.
|
|
342
343
|
* Use as a type guard when you need to ensure authentication.
|
|
343
344
|
*
|
|
344
|
-
* @throws {
|
|
345
|
+
* @throws {Error} if not authenticated
|
|
345
346
|
*
|
|
346
347
|
* @example
|
|
347
348
|
* ```typescript
|
|
@@ -351,7 +352,7 @@ export class Client implements Agent {
|
|
|
351
352
|
* ```
|
|
352
353
|
*/
|
|
353
354
|
public assertAuthenticated(): asserts this is { did: DidString } {
|
|
354
|
-
if (!this.did) throw new
|
|
355
|
+
if (!this.did) throw new Error('Client is not authenticated')
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
/**
|
|
@@ -379,11 +380,18 @@ export class Client implements Agent {
|
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
/**
|
|
382
|
-
*
|
|
383
|
+
* {@link Agent}'s {@link Agent.fetchHandler} implementation, which adds
|
|
384
|
+
* labelers and service proxying headers. This method allow a {@link Client}
|
|
385
|
+
* instance to be used directly as an {@link Agent} for another
|
|
386
|
+
* {@link Client}, enabling composition of headers (labelers, proxying, etc.).
|
|
387
|
+
*
|
|
383
388
|
* @param path - The request path
|
|
384
389
|
* @param init - Request initialization options
|
|
385
390
|
*/
|
|
386
|
-
public fetchHandler(
|
|
391
|
+
public fetchHandler(
|
|
392
|
+
path: `/${string}`,
|
|
393
|
+
init: RequestInit,
|
|
394
|
+
): Promise<Response> {
|
|
387
395
|
const headers = buildAtprotoHeaders({
|
|
388
396
|
headers: init.headers,
|
|
389
397
|
service: this.service,
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'
|
|
3
|
+
import {
|
|
4
|
+
XrpcAuthenticationError,
|
|
5
|
+
XrpcInternalError,
|
|
6
|
+
XrpcInvalidResponseError,
|
|
7
|
+
XrpcResponseError,
|
|
8
|
+
XrpcUpstreamError,
|
|
9
|
+
asXrpcFailure,
|
|
10
|
+
} from './errors.js'
|
|
11
|
+
|
|
12
|
+
// Minimal method fixture
|
|
13
|
+
const testQuery = l.query(
|
|
14
|
+
'io.example.test',
|
|
15
|
+
l.params(),
|
|
16
|
+
l.payload('application/json', l.object({ value: l.string() })),
|
|
17
|
+
['TestError', 'AnotherError'],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const testQueryNoErrors = l.query(
|
|
21
|
+
'io.example.noErrors',
|
|
22
|
+
l.params(),
|
|
23
|
+
l.payload('application/json', l.object({ value: l.string() })),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// XrpcResponseError
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
describe(XrpcResponseError, () => {
|
|
31
|
+
function createResponseError(
|
|
32
|
+
status: number,
|
|
33
|
+
errorCode: string,
|
|
34
|
+
message?: string,
|
|
35
|
+
) {
|
|
36
|
+
const response = new Response(null, { status })
|
|
37
|
+
return new XrpcResponseError(testQuery, response, {
|
|
38
|
+
encoding: 'application/json',
|
|
39
|
+
body: { error: errorCode, message },
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('exposes status from the response', () => {
|
|
44
|
+
const err = createResponseError(404, 'NotFound')
|
|
45
|
+
expect(err.status).toBe(404)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('exposes headers from the response', () => {
|
|
49
|
+
const response = new Response(null, {
|
|
50
|
+
status: 400,
|
|
51
|
+
headers: { 'X-Test': 'value' },
|
|
52
|
+
})
|
|
53
|
+
const err = new XrpcResponseError(testQuery, response, {
|
|
54
|
+
encoding: 'application/json',
|
|
55
|
+
body: { error: 'TestError' },
|
|
56
|
+
})
|
|
57
|
+
expect(err.headers.get('X-Test')).toBe('value')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('exposes body from the payload', () => {
|
|
61
|
+
const err = createResponseError(400, 'TestError', 'details')
|
|
62
|
+
expect(err.body).toEqual({ error: 'TestError', message: 'details' })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('toDownstreamError', () => {
|
|
66
|
+
it('returns 502 for 5xx upstream errors', () => {
|
|
67
|
+
const err = createResponseError(
|
|
68
|
+
500,
|
|
69
|
+
'InternalServerError',
|
|
70
|
+
'Upstream crashed',
|
|
71
|
+
)
|
|
72
|
+
const downstream = err.toDownstreamError()
|
|
73
|
+
|
|
74
|
+
expect(downstream.status).toBe(502)
|
|
75
|
+
expect(downstream.body).toEqual({
|
|
76
|
+
error: 'InternalServerError',
|
|
77
|
+
message: 'Upstream crashed',
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('preserves original status for 4xx errors', () => {
|
|
82
|
+
const err = createResponseError(404, 'NotFound', 'Record not found')
|
|
83
|
+
const downstream = err.toDownstreamError()
|
|
84
|
+
|
|
85
|
+
expect(downstream.status).toBe(404)
|
|
86
|
+
expect(downstream.body).toEqual({
|
|
87
|
+
error: 'NotFound',
|
|
88
|
+
message: 'Record not found',
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('toJSON', () => {
|
|
94
|
+
it('returns the payload body', () => {
|
|
95
|
+
const err = createResponseError(400, 'TestError', 'message')
|
|
96
|
+
expect(err.toJSON()).toEqual({ error: 'TestError', message: 'message' })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('matchesSchemaErrors', () => {
|
|
101
|
+
it('returns true when error matches method declared errors', () => {
|
|
102
|
+
const err = createResponseError(400, 'TestError')
|
|
103
|
+
expect(err.matchesSchemaErrors()).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('returns false for undeclared error codes', () => {
|
|
107
|
+
const err = createResponseError(400, 'UnknownError')
|
|
108
|
+
expect(err.matchesSchemaErrors()).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns false when method has no declared errors', () => {
|
|
112
|
+
const response = new Response(null, { status: 400 })
|
|
113
|
+
const err = new XrpcResponseError(testQueryNoErrors, response, {
|
|
114
|
+
encoding: 'application/json',
|
|
115
|
+
body: { error: 'SomeError' },
|
|
116
|
+
})
|
|
117
|
+
expect(err.matchesSchemaErrors()).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('shouldRetry', () => {
|
|
122
|
+
it('returns true for retryable status codes', () => {
|
|
123
|
+
expect(createResponseError(429, 'RateLimit').shouldRetry()).toBe(true)
|
|
124
|
+
expect(createResponseError(500, 'Internal').shouldRetry()).toBe(true)
|
|
125
|
+
expect(createResponseError(502, 'BadGateway').shouldRetry()).toBe(true)
|
|
126
|
+
expect(createResponseError(503, 'Unavailable').shouldRetry()).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns false for non-retryable status codes', () => {
|
|
130
|
+
expect(createResponseError(400, 'BadRequest').shouldRetry()).toBe(false)
|
|
131
|
+
expect(createResponseError(401, 'Unauthorized').shouldRetry()).toBe(false)
|
|
132
|
+
expect(createResponseError(404, 'NotFound').shouldRetry()).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// XrpcAuthenticationError
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
describe(XrpcAuthenticationError, () => {
|
|
142
|
+
it('is never retryable', () => {
|
|
143
|
+
const response = new Response(null, { status: 401 })
|
|
144
|
+
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
145
|
+
encoding: 'application/json',
|
|
146
|
+
body: { error: 'AuthenticationRequired' },
|
|
147
|
+
})
|
|
148
|
+
expect(err.shouldRetry()).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('parses WWW-Authenticate header', () => {
|
|
152
|
+
const response = new Response(null, {
|
|
153
|
+
status: 401,
|
|
154
|
+
headers: {
|
|
155
|
+
'WWW-Authenticate': 'Bearer realm="api", error="InvalidToken"',
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
159
|
+
encoding: 'application/json',
|
|
160
|
+
body: { error: 'AuthenticationRequired' },
|
|
161
|
+
})
|
|
162
|
+
expect(err.wwwAuthenticate).toHaveProperty('Bearer')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('returns empty object when no WWW-Authenticate header', () => {
|
|
166
|
+
const response = new Response(null, { status: 401 })
|
|
167
|
+
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
168
|
+
encoding: 'application/json',
|
|
169
|
+
body: { error: 'AuthenticationRequired' },
|
|
170
|
+
})
|
|
171
|
+
expect(err.wwwAuthenticate).toEqual({})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('toDownstreamError always returns 401', () => {
|
|
175
|
+
const response = new Response(null, { status: 401 })
|
|
176
|
+
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
177
|
+
encoding: 'application/json',
|
|
178
|
+
body: { error: 'AuthenticationRequired', message: 'No token' },
|
|
179
|
+
})
|
|
180
|
+
const downstream = err.toDownstreamError()
|
|
181
|
+
|
|
182
|
+
expect(downstream.status).toBe(401)
|
|
183
|
+
expect(downstream.body).toEqual({
|
|
184
|
+
error: 'AuthenticationRequired',
|
|
185
|
+
message: 'No token',
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// XrpcUpstreamError
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
describe(XrpcUpstreamError, () => {
|
|
195
|
+
it('has error code UpstreamFailure', () => {
|
|
196
|
+
const response = new Response(null, { status: 200 })
|
|
197
|
+
const err = new XrpcUpstreamError(testQuery, response)
|
|
198
|
+
expect(err.error).toBe('UpstreamFailure')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('toDownstreamError returns 502', () => {
|
|
202
|
+
const response = new Response(null, { status: 200 })
|
|
203
|
+
const err = new XrpcUpstreamError(testQuery, response)
|
|
204
|
+
const downstream = err.toDownstreamError()
|
|
205
|
+
expect(downstream.status).toBe(502)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('shouldRetry is true for retryable status codes', () => {
|
|
209
|
+
const response = new Response(null, { status: 502 })
|
|
210
|
+
const err = new XrpcUpstreamError(testQuery, response)
|
|
211
|
+
expect(err.shouldRetry()).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('shouldRetry is false for non-retryable status codes', () => {
|
|
215
|
+
const response = new Response(null, { status: 200 })
|
|
216
|
+
const err = new XrpcUpstreamError(testQuery, response)
|
|
217
|
+
expect(err.shouldRetry()).toBe(false)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// XrpcInvalidResponseError
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
describe(XrpcInvalidResponseError, () => {
|
|
226
|
+
it('extends XrpcUpstreamError', () => {
|
|
227
|
+
const response = new Response(null, { status: 200 })
|
|
228
|
+
const validationError = new LexValidationError([
|
|
229
|
+
new IssueInvalidType([], 42, ['string']),
|
|
230
|
+
])
|
|
231
|
+
const err = new XrpcInvalidResponseError(
|
|
232
|
+
testQuery,
|
|
233
|
+
response,
|
|
234
|
+
{ encoding: 'application/json', body: { value: 42 } },
|
|
235
|
+
validationError,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
expect(err).toBeInstanceOf(XrpcUpstreamError)
|
|
239
|
+
expect(err.error).toBe('UpstreamFailure')
|
|
240
|
+
expect(err.cause).toBe(validationError)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('includes validation error message', () => {
|
|
244
|
+
const validationError = new LexValidationError([
|
|
245
|
+
new IssueInvalidType([], 42, ['string']),
|
|
246
|
+
])
|
|
247
|
+
const err = new XrpcInvalidResponseError(
|
|
248
|
+
testQuery,
|
|
249
|
+
new Response(null, { status: 200 }),
|
|
250
|
+
{ encoding: 'application/json', body: { value: 42 } },
|
|
251
|
+
validationError,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
expect(err.message).toContain('Invalid response:')
|
|
255
|
+
expect(err.message).toContain(validationError.message)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('toDownstreamError returns 502', () => {
|
|
259
|
+
const validationError = new LexValidationError([
|
|
260
|
+
new IssueInvalidType([], 42, ['string']),
|
|
261
|
+
])
|
|
262
|
+
const err = new XrpcInvalidResponseError(
|
|
263
|
+
testQuery,
|
|
264
|
+
new Response(null, { status: 200 }),
|
|
265
|
+
{ encoding: 'application/json', body: { value: 42 } },
|
|
266
|
+
validationError,
|
|
267
|
+
)
|
|
268
|
+
const downstream = err.toDownstreamError()
|
|
269
|
+
expect(downstream.status).toBe(502)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// XrpcInternalError
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
describe(XrpcInternalError, () => {
|
|
278
|
+
it('has error code InternalServerError', () => {
|
|
279
|
+
const err = new XrpcInternalError(testQuery)
|
|
280
|
+
expect(err.error).toBe('InternalServerError')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('is always retryable', () => {
|
|
284
|
+
const err = new XrpcInternalError(testQuery)
|
|
285
|
+
expect(err.shouldRetry()).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('toJSON does not expose internal details', () => {
|
|
289
|
+
const err = new XrpcInternalError(
|
|
290
|
+
testQuery,
|
|
291
|
+
'Secret database connection string leaked',
|
|
292
|
+
)
|
|
293
|
+
const json = err.toJSON()
|
|
294
|
+
|
|
295
|
+
expect(json.error).toBe('InternalServerError')
|
|
296
|
+
expect(json.message).toBe('Internal Server Error')
|
|
297
|
+
expect(json.message).not.toContain('Secret')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('toDownstreamError returns 500', () => {
|
|
301
|
+
const err = new XrpcInternalError(testQuery, 'internal details')
|
|
302
|
+
const downstream = err.toDownstreamError()
|
|
303
|
+
|
|
304
|
+
expect(downstream.status).toBe(500)
|
|
305
|
+
expect(downstream.body.error).toBe('InternalServerError')
|
|
306
|
+
expect(downstream.body.message).toBe('Internal Server Error')
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// asXrpcFailure
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
describe('asXrpcFailure', () => {
|
|
315
|
+
it('returns existing XrpcResponseError for the same method', () => {
|
|
316
|
+
const response = new Response(null, { status: 400 })
|
|
317
|
+
const err = new XrpcResponseError(testQuery, response, {
|
|
318
|
+
encoding: 'application/json',
|
|
319
|
+
body: { error: 'TestError' },
|
|
320
|
+
})
|
|
321
|
+
expect(asXrpcFailure(testQuery, err)).toBe(err)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('wraps unknown errors in XrpcInternalError', () => {
|
|
325
|
+
const err = new TypeError('fetch failed')
|
|
326
|
+
const failure = asXrpcFailure(testQuery, err)
|
|
327
|
+
|
|
328
|
+
expect(failure).toBeInstanceOf(XrpcInternalError)
|
|
329
|
+
expect(failure.cause).toBe(err)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('wraps XrpcError for a different method in XrpcInternalError', () => {
|
|
333
|
+
const otherQuery = l.query(
|
|
334
|
+
'io.example.other',
|
|
335
|
+
l.params(),
|
|
336
|
+
l.payload('application/json', l.object({ value: l.string() })),
|
|
337
|
+
)
|
|
338
|
+
const response = new Response(null, { status: 400 })
|
|
339
|
+
const err = new XrpcResponseError(otherQuery, response, {
|
|
340
|
+
encoding: 'application/json',
|
|
341
|
+
body: { error: 'TestError' },
|
|
342
|
+
})
|
|
343
|
+
const failure = asXrpcFailure(testQuery, err)
|
|
344
|
+
expect(failure).toBeInstanceOf(XrpcInternalError)
|
|
345
|
+
})
|
|
346
|
+
})
|