better-call 1.0.27 → 1.0.28-beta.2
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 +87 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/index.cjs +32 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +32 -20
- package/dist/index.js.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{router-D1f_-c2B.d.ts → router-NaFkuy-s.d.ts} +31 -1
- package/dist/{router-DxWRTWmk.d.cts → router-rGV6mTr8.d.cts} +31 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,6 +231,93 @@ const createItem = createEndpoint("/item", {
|
|
|
231
231
|
})
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
+
#### Allowed Media Types
|
|
235
|
+
|
|
236
|
+
You can restrict which media types (MIME types) are allowed for request bodies using the `allowedMediaTypes` option. This can be configured at both the router level and the endpoint level, with endpoint-level configuration taking precedence.
|
|
237
|
+
|
|
238
|
+
When a request is made with a disallowed media type, the endpoint will return a `415 Unsupported Media Type` error.
|
|
239
|
+
|
|
240
|
+
**Router-level configuration:**
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const router = createRouter({
|
|
244
|
+
createItem,
|
|
245
|
+
updateItem
|
|
246
|
+
}, {
|
|
247
|
+
// All endpoints in this router will only accept JSON
|
|
248
|
+
allowedMediaTypes: ["application/json"]
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Endpoint-level configuration:**
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const uploadFile = createEndpoint("/upload", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
metadata: {
|
|
258
|
+
// This endpoint will only accept form data
|
|
259
|
+
allowedMediaTypes: ["multipart/form-data"]
|
|
260
|
+
}
|
|
261
|
+
}, async (ctx) => {
|
|
262
|
+
return { success: true }
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Multiple media types:**
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const createItem = createEndpoint("/item", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
body: z.object({
|
|
272
|
+
id: z.string()
|
|
273
|
+
}),
|
|
274
|
+
metadata: {
|
|
275
|
+
// Accept both JSON and form-urlencoded
|
|
276
|
+
allowedMediaTypes: [
|
|
277
|
+
"application/json",
|
|
278
|
+
"application/x-www-form-urlencoded"
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
}, async (ctx) => {
|
|
282
|
+
return {
|
|
283
|
+
item: {
|
|
284
|
+
id: ctx.body.id
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Endpoint overriding router:**
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const router = createRouter({
|
|
294
|
+
createItem,
|
|
295
|
+
uploadFile
|
|
296
|
+
}, {
|
|
297
|
+
// Default: only accept JSON
|
|
298
|
+
allowedMediaTypes: ["application/json"]
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const uploadFile = createEndpoint("/upload", {
|
|
302
|
+
method: "POST",
|
|
303
|
+
metadata: {
|
|
304
|
+
// This endpoint overrides the router setting
|
|
305
|
+
allowedMediaTypes: ["multipart/form-data", "application/octet-stream"]
|
|
306
|
+
}
|
|
307
|
+
}, async (ctx) => {
|
|
308
|
+
return { success: true }
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Common media types:
|
|
313
|
+
- `application/json` - JSON data
|
|
314
|
+
- `application/x-www-form-urlencoded` - Form data
|
|
315
|
+
- `multipart/form-data` - File uploads
|
|
316
|
+
- `text/plain` - Plain text
|
|
317
|
+
- `application/octet-stream` - Binary data
|
|
318
|
+
|
|
319
|
+
> **Note:** The validation is case-insensitive and handles charset parameters automatically (e.g., `application/json; charset=utf-8` will match `application/json`).
|
|
320
|
+
|
|
234
321
|
#### Require Headers
|
|
235
322
|
|
|
236
323
|
The `requireHeaders` option is used to require the request to have headers. If the request doesn't have headers, the endpoint will throw an error. This is only useful when you call the endpoint as a function.
|
package/dist/client.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-
|
|
1
|
+
import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-rGV6mTr8.cjs";
|
|
2
2
|
import { BetterFetchOption, BetterFetchResponse } from "@better-fetch/fetch";
|
|
3
3
|
|
|
4
4
|
//#region src/client.d.ts
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-
|
|
1
|
+
import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-NaFkuy-s.js";
|
|
2
2
|
import { BetterFetchOption, BetterFetchResponse } from "@better-fetch/fetch";
|
|
3
3
|
|
|
4
4
|
//#region src/client.d.ts
|
package/dist/index.cjs
CHANGED
|
@@ -127,11 +127,22 @@ const APIError = makeErrorForHideStackFrame(InternalAPIError, Error);
|
|
|
127
127
|
|
|
128
128
|
//#endregion
|
|
129
129
|
//#region src/utils.ts
|
|
130
|
-
async function getBody(request) {
|
|
130
|
+
async function getBody(request, allowedMediaTypes) {
|
|
131
131
|
const contentType = request.headers.get("content-type") || "";
|
|
132
|
+
const normalizedContentType = contentType.toLowerCase();
|
|
132
133
|
if (!request.body) return;
|
|
133
|
-
if (
|
|
134
|
-
|
|
134
|
+
if (allowedMediaTypes && allowedMediaTypes.length > 0) {
|
|
135
|
+
if (!allowedMediaTypes.some((allowed) => {
|
|
136
|
+
const normalizedContentTypeBase = normalizedContentType.split(";")[0].trim();
|
|
137
|
+
const normalizedAllowed = allowed.toLowerCase().trim();
|
|
138
|
+
return normalizedContentTypeBase === normalizedAllowed || normalizedContentTypeBase.includes(normalizedAllowed);
|
|
139
|
+
})) throw new APIError(415, {
|
|
140
|
+
message: `Content-Type "${contentType}" is not allowed. Allowed types: ${allowedMediaTypes.join(", ")}`,
|
|
141
|
+
code: "UNSUPPORTED_MEDIA_TYPE"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (normalizedContentType.includes("application/json")) return await request.json();
|
|
145
|
+
if (normalizedContentType.includes("application/x-www-form-urlencoded")) {
|
|
135
146
|
const formData = await request.formData();
|
|
136
147
|
const result = {};
|
|
137
148
|
formData.forEach((value, key) => {
|
|
@@ -139,7 +150,7 @@ async function getBody(request) {
|
|
|
139
150
|
});
|
|
140
151
|
return result;
|
|
141
152
|
}
|
|
142
|
-
if (
|
|
153
|
+
if (normalizedContentType.includes("multipart/form-data")) {
|
|
143
154
|
const formData = await request.formData();
|
|
144
155
|
const result = {};
|
|
145
156
|
formData.forEach((value, key) => {
|
|
@@ -147,10 +158,10 @@ async function getBody(request) {
|
|
|
147
158
|
});
|
|
148
159
|
return result;
|
|
149
160
|
}
|
|
150
|
-
if (
|
|
151
|
-
if (
|
|
152
|
-
if (
|
|
153
|
-
if (
|
|
161
|
+
if (normalizedContentType.includes("text/plain")) return await request.text();
|
|
162
|
+
if (normalizedContentType.includes("application/octet-stream")) return await request.arrayBuffer();
|
|
163
|
+
if (normalizedContentType.includes("application/pdf") || normalizedContentType.includes("image/") || normalizedContentType.includes("video/")) return await request.blob();
|
|
164
|
+
if (normalizedContentType.includes("application/stream") || request.body instanceof ReadableStream) return request.body;
|
|
154
165
|
return await request.text();
|
|
155
166
|
}
|
|
156
167
|
function isAPIError(error) {
|
|
@@ -2491,19 +2502,20 @@ const createRouter = (endpoints, config$1) => {
|
|
|
2491
2502
|
else query[key] = value;
|
|
2492
2503
|
});
|
|
2493
2504
|
const handler = route.data;
|
|
2494
|
-
const context = {
|
|
2495
|
-
path,
|
|
2496
|
-
method: request.method,
|
|
2497
|
-
headers: request.headers,
|
|
2498
|
-
params: route.params ? JSON.parse(JSON.stringify(route.params)) : {},
|
|
2499
|
-
request,
|
|
2500
|
-
body: handler.options.disableBody ? void 0 : await getBody(handler.options.cloneRequest ? request.clone() : request),
|
|
2501
|
-
query,
|
|
2502
|
-
_flag: "router",
|
|
2503
|
-
asResponse: true,
|
|
2504
|
-
context: config$1?.routerContext
|
|
2505
|
-
};
|
|
2506
2505
|
try {
|
|
2506
|
+
const allowedMediaTypes = handler.options.metadata?.allowedMediaTypes || config$1?.allowedMediaTypes;
|
|
2507
|
+
const context = {
|
|
2508
|
+
path,
|
|
2509
|
+
method: request.method,
|
|
2510
|
+
headers: request.headers,
|
|
2511
|
+
params: route.params ? JSON.parse(JSON.stringify(route.params)) : {},
|
|
2512
|
+
request,
|
|
2513
|
+
body: handler.options.disableBody ? void 0 : await getBody(handler.options.cloneRequest ? request.clone() : request, allowedMediaTypes),
|
|
2514
|
+
query,
|
|
2515
|
+
_flag: "router",
|
|
2516
|
+
asResponse: true,
|
|
2517
|
+
context: config$1?.routerContext
|
|
2518
|
+
};
|
|
2507
2519
|
const middlewareRoutes = (0, rou3.findAllRoutes)(middlewareRouter, "*", path);
|
|
2508
2520
|
if (middlewareRoutes?.length) for (const { data: middleware, params } of middlewareRoutes) {
|
|
2509
2521
|
const res = await middleware({
|