@springbrand/http 0.1.0-alpha.4 → 0.1.0-alpha.6
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/.agents/skills/dune-react-http/SKILL.md +4 -2
- package/dist/catalog.js +4 -0
- package/dist/client.js +1 -0
- package/dist/server/body.js +4 -3
- package/dist/server/response.js +2 -1
- package/package.json +11 -10
|
@@ -5,6 +5,8 @@ description: Use SpringBrand shared HTTP contracts, Axios request clients, Query
|
|
|
5
5
|
|
|
6
6
|
# Dune React HTTP
|
|
7
7
|
|
|
8
|
+
Shared error codes, canonical statuses and their HTTP statuses are defined in `@springbrand/errors`; this package reads them and does not restate them.
|
|
9
|
+
|
|
8
10
|
Read the installed declarations and README before configuring a client. Public entry points own different runtime needs; import only the capability needed.
|
|
9
11
|
|
|
10
12
|
| Entry point | Responsibility |
|
|
@@ -13,7 +15,7 @@ Read the installed declarations and README before configuring a client. Public e
|
|
|
13
15
|
| @springbrand/http/errors | Error definitions, cancellation and transient failure classification |
|
|
14
16
|
| @springbrand/http/client | Axios client and standard HTTP error parsing |
|
|
15
17
|
| @springbrand/http/query | Optional TanStack React Query integration |
|
|
16
|
-
| @springbrand/http/catalog | Project error catalog: typed HttpError, Zod response schemas, client decoder (zod peer) |
|
|
18
|
+
| @springbrand/http/catalog | Project error catalog: typed HttpError, Zod response schemas, client decoder (zod peer). Codes registered in `@springbrand/errors` must use the registered HTTP status |
|
|
17
19
|
| @springbrand/http/hono | Hono responders with TypedResponse types for zod-openapi (hono peer) |
|
|
18
20
|
| @springbrand/http/types | Type-only entry for every public type, so consumers emitting declarations stay portable |
|
|
19
21
|
| @springbrand/http/server | Standard Response helpers, bounded body reading, If-Match, Content-Disposition, log diagnostics and service authentication |
|
|
@@ -24,7 +26,7 @@ Create one client per trusted API origin. Inject getToken; do not copy Token sto
|
|
|
24
26
|
|
|
25
27
|
Pass params for query serialization and signal/timeoutMs for cancellation/deadlines. Null/undefined query values are omitted; use a string for literal null. Do not duplicate query encoding or timeout controllers in domain clients.
|
|
26
28
|
|
|
27
|
-
Use get/post/put/patch/delete for payloads, request for data plus status and headers. Explicitly choose json, text or blob for endpoints without the standard success envelope. FormData owns its boundary. Pass Query's signal through; do not wrap cancellation as network failure.
|
|
29
|
+
Use get/post/put/patch/delete for payloads, request for data plus status and headers. Explicitly choose json, text or blob for endpoints without the standard success envelope. FormData owns its boundary. The client sends no User-Agent of its own (Safari and Firefox would otherwise require every cross-origin API to allow it in CORS); set one explicitly only for a server-to-server call that needs it. Pass Query's signal through; do not wrap cancellation as network failure.
|
|
28
30
|
|
|
29
31
|
## Error handling
|
|
30
32
|
|
package/dist/catalog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRegisteredErrorCode, httpStatusOf } from "@springbrand/errors";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { ApiError } from "./errors.js";
|
|
3
4
|
import { parseHttpError } from "./response.js";
|
|
@@ -24,6 +25,9 @@ function defineErrorCatalog(definitions, options = {}) {
|
|
|
24
25
|
if (definition.status !== 0 && !(Number.isInteger(definition.status) && definition.status >= 400 && definition.status <= 599)) {
|
|
25
26
|
throw new RangeError(`Error ${code} must use status 0 or a 4xx/5xx status`);
|
|
26
27
|
}
|
|
28
|
+
if (isRegisteredErrorCode(code) && definition.status !== (httpStatusOf(code) ?? 0)) {
|
|
29
|
+
throw new RangeError(`Error ${code} must use status ${httpStatusOf(code) ?? 0} registered in @springbrand/errors`);
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
const codes = Object.keys(definitions).filter((code) => definitions[code].status !== 0);
|
|
29
33
|
for (const code of Object.keys(detailSchemas)) {
|
package/dist/client.js
CHANGED
|
@@ -57,6 +57,7 @@ function createHttpClient(config) {
|
|
|
57
57
|
if (options.body instanceof FormData) headers.delete("Content-Type");
|
|
58
58
|
if (timeout > 0) timer = setTimeout(() => controller.abort(sdkError("REQUEST_TIMEOUT", { status: 0, kind: "timeout" })), timeout);
|
|
59
59
|
const requestHeaders = new axios.AxiosHeaders(Object.fromEntries(headers));
|
|
60
|
+
if (!requestHeaders.has("User-Agent")) requestHeaders.set("User-Agent", false);
|
|
60
61
|
if (options.body instanceof FormData) requestHeaders.setContentType("multipart/form-data");
|
|
61
62
|
const response = await transport.request({
|
|
62
63
|
url: url.href,
|
package/dist/server/body.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { httpStatusOf } from "@springbrand/errors";
|
|
1
2
|
import { ApiError } from "../errors.js";
|
|
2
|
-
const invalidJson = (cause) => new ApiError({ status:
|
|
3
|
-
const tooLarge = () => new ApiError({ status:
|
|
3
|
+
const invalidJson = (cause) => new ApiError({ status: httpStatusOf("REQUEST_INVALID_JSON"), code: "REQUEST_INVALID_JSON", message: "Expected a JSON object", kind: "http", cause });
|
|
4
|
+
const tooLarge = () => new ApiError({ status: httpStatusOf("REQUEST_PAYLOAD_TOO_LARGE"), code: "REQUEST_PAYLOAD_TOO_LARGE", message: "Request body is too large", kind: "http" });
|
|
4
5
|
async function readJson(request) {
|
|
5
6
|
try {
|
|
6
7
|
return await request.json();
|
|
@@ -53,7 +54,7 @@ function requireIfMatchRevision(request, subject) {
|
|
|
53
54
|
const match = /^"([1-9]\d*)"$/.exec(request.headers.get("if-match") ?? "");
|
|
54
55
|
const revision = match ? Number(match[1]) : Number.NaN;
|
|
55
56
|
if (!Number.isSafeInteger(revision)) {
|
|
56
|
-
throw new ApiError({ status:
|
|
57
|
+
throw new ApiError({ status: httpStatusOf("REQUEST_INVALID_INPUT"), code: "REQUEST_INVALID_INPUT", message: `If-Match must contain the current ${subject}, for example "3"`, kind: "http" });
|
|
57
58
|
}
|
|
58
59
|
return revision;
|
|
59
60
|
}
|
package/dist/server/response.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { httpStatusOf } from "@springbrand/errors";
|
|
1
2
|
import { ApiError, errorDefinitions } from "../errors.js";
|
|
2
3
|
function responseHeaders(options) {
|
|
3
4
|
const requestId = options.requestId ?? crypto.randomUUID();
|
|
@@ -22,7 +23,7 @@ function toErrorResponse(error, options = {}) {
|
|
|
22
23
|
if (error instanceof ApiError && error.kind === "http" && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 && /^[A-Z][A-Z0-9_]{0,127}$/.test(error.code) && !["NETWORK_ERROR", "REQUEST_TIMEOUT", "RESPONSE_TOO_LARGE"].includes(error.code)) {
|
|
23
24
|
return fail({ status: error.status, code: error.code, message: error.message, errors: error.errors, details: error.details, requestId: options.requestId ?? error.requestId, headers: options.headers });
|
|
24
25
|
}
|
|
25
|
-
return fail({ status:
|
|
26
|
+
return fail({ status: httpStatusOf("INTERNAL_ERROR"), code: "INTERNAL_ERROR", message: errorDefinitions.INTERNAL_ERROR, ...options });
|
|
26
27
|
}
|
|
27
28
|
export {
|
|
28
29
|
fail,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/http",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -58,16 +58,10 @@
|
|
|
58
58
|
},
|
|
59
59
|
"./package.json": "./package.json"
|
|
60
60
|
},
|
|
61
|
-
"scripts": {
|
|
62
|
-
"build": "vite build && tsc -p tsconfig.build.json",
|
|
63
|
-
"watch": "vite build --watch",
|
|
64
|
-
"typecheck": "tsc --noEmit",
|
|
65
|
-
"lint": "eslint src tests --ext ts --max-warnings 0",
|
|
66
|
-
"test": "vitest run"
|
|
67
|
-
},
|
|
68
61
|
"dependencies": {
|
|
69
62
|
"axios": "1.20.0",
|
|
70
|
-
"content-disposition": "2.0.1"
|
|
63
|
+
"content-disposition": "2.0.1",
|
|
64
|
+
"@springbrand/errors": "^0.1.0-alpha.0"
|
|
71
65
|
},
|
|
72
66
|
"peerDependencies": {
|
|
73
67
|
"@tanstack/react-query": "^5.104.1",
|
|
@@ -95,5 +89,12 @@
|
|
|
95
89
|
"vite": "^5.4.19",
|
|
96
90
|
"vitest": "3.2.6",
|
|
97
91
|
"zod": "4.6.5"
|
|
92
|
+
},
|
|
93
|
+
"scripts": {
|
|
94
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
95
|
+
"watch": "vite build --watch",
|
|
96
|
+
"typecheck": "tsc --noEmit",
|
|
97
|
+
"lint": "eslint src tests --ext ts --max-warnings 0",
|
|
98
|
+
"test": "vitest run"
|
|
98
99
|
}
|
|
99
|
-
}
|
|
100
|
+
}
|