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 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-DxWRTWmk.cjs";
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-D1f_-c2B.js";
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 (contentType.includes("application/json")) return await request.json();
134
- if (contentType.includes("application/x-www-form-urlencoded")) {
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 (contentType.includes("multipart/form-data")) {
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 (contentType.includes("text/plain")) return await request.text();
151
- if (contentType.includes("application/octet-stream")) return await request.arrayBuffer();
152
- if (contentType.includes("application/pdf") || contentType.includes("image/") || contentType.includes("video/")) return await request.blob();
153
- if (contentType.includes("application/stream") || request.body instanceof ReadableStream) return request.body;
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({