Skip to content

Commit

Permalink
Fix request body & path param parsing
Browse files Browse the repository at this point in the history
This fixes a bug where the Zod-parsed request
body accessed via `req.json()` had potentially
incorrect types due to JSON serialization. Now
the `json()` method is overridden so that the parsed
data is returned as is without the JSON serialization.

Additionally, the request path parameters are now also
validated when using app router when previously they
were used only for providing types for the used path
parameters.
  • Loading branch information
blomqma committed Mar 26, 2024
1 parent c35d411 commit 1dc6506
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 21 deletions.
30 changes: 30 additions & 0 deletions apps/example/src/app/api/v2/route-with-path-params/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { z } from 'zod';

const paramsSchema = z.object({
slug: z.enum(['foo', 'bar', 'baz'])
});

export const runtime = 'edge';

export const { GET } = route({
getPathParams: routeOperation({
method: 'GET'
})
.input({
contentType: 'application/json',
params: paramsSchema
})
.outputs([
{
status: 200,
contentType: 'application/json',
body: paramsSchema
}
])
.handler((_req, { params: { slug } }) => {
return TypedNextResponse.json({
slug
});
})
});
8 changes: 2 additions & 6 deletions apps/example/src/app/api/v2/route-with-query-params/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { z } from 'zod';

const querySchema = z.object({
foo: z.string().uuid(),
bar: z.string().optional(),
baz: z.string()
total: z.string()
});

export const runtime = 'edge';
Expand All @@ -28,9 +26,7 @@ export const { GET } = route({
const query = req.nextUrl.searchParams;

return TypedNextResponse.json({
foo: query.get('foo') ?? '',
bar: query.get('bar') ?? '',
baz: query.get('baz') ?? ''
total: query.get('total') ?? ''
});
})
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { apiRoute, apiRouteOperation } from 'next-rest-framework';
import { z } from 'zod';

const paramsSchema = z.object({
slug: z.enum(['foo', 'bar', 'baz'])
});

export default apiRoute({
getQueryParams: apiRouteOperation({
method: 'GET'
})
.input({
contentType: 'application/json',
query: paramsSchema
})
.outputs([
{
status: 200,
contentType: 'application/json',
body: paramsSchema
}
])
.handler((req, res) => {
res.json(req.query);
})
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { apiRoute, apiRouteOperation } from 'next-rest-framework';
import { z } from 'zod';

const querySchema = z.object({
foo: z.string().uuid(),
bar: z.string().optional(),
baz: z.string()
total: z.string()
});

export default apiRoute({
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The route operation input function is used for type-checking, validation and doc
| `bodySchema` | A JSON schema that you can provide in case the conversion of the `body` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` |
| `query` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the query parameters. When the query schema is defined, a request with invalid query parameters will get an error response. Query parameters are parsed using this schema and updated to the request if valid, so the query parameters from the request should always match the schema. | `false` |
| `querySchema` | A JSON schema that you can provide in case the conversion of the `query` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` |
| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters for strong typing when using them in your route handler. | `false` |
| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters. When the params schema is defined, a request with invalid path parameters will get an error response. Path parameters are parsed using this schema and updated to the request if valid, so the path parameters from the request should always match the schema. | `false` |

Calling the route operation input function allows you to chain your API handler logic with the [Route operation outputs](#route-operation-outputs), [Route operation middleware](#route-operation-middleware) and [Route operation handler](#route-operation-handler) functions.

Expand Down
2 changes: 1 addition & 1 deletion packages/next-rest-framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ The route operation input function is used for type-checking, validation and doc
| `bodySchema` | A JSON schema that you can provide in case the conversion of the `body` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` |
| `query` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the query parameters. When the query schema is defined, a request with invalid query parameters will get an error response. Query parameters are parsed using this schema and updated to the request if valid, so the query parameters from the request should always match the schema. | `false` |
| `querySchema` | A JSON schema that you can provide in case the conversion of the `query` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` |
| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters for strong typing when using them in your route handler. | `false` |
| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters. When the params schema is defined, a request with invalid path parameters will get an error response. Path parameters are parsed using this schema and updated to the request if valid, so the path parameters from the request should always match the schema. | `false` |

Calling the route operation input function allows you to chain your API handler logic with the [Route operation outputs](#route-operation-outputs), [Route operation middleware](#route-operation-middleware) and [Route operation handler](#route-operation-handler) functions.

Expand Down
54 changes: 45 additions & 9 deletions packages/next-rest-framework/src/app-router/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
const { input, handler, middleware1, middleware2, middleware3 } =
operation;

let reqClone = req.clone() as NextRequest;
let reqClone = new NextRequest(req.url, {
method: req.method,
headers: req.headers,
body: req.body
});

let middlewareOptions: BaseOptions = {};

Expand Down Expand Up @@ -94,7 +98,8 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
const {
body: bodySchema,
query: querySchema,
contentType: contentTypeSchema
contentType: contentTypeSchema,
params: paramsSchema
} = input;

const contentType = req.headers.get('content-type')?.split(';')[0];
Expand All @@ -109,7 +114,7 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
if (bodySchema) {
if (contentType === 'application/json') {
try {
const json = await req.clone().json();
const json = await reqClone.json();

const { valid, errors, data } = validateSchema({
schema: bodySchema,
Expand All @@ -129,11 +134,12 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
}

reqClone = new NextRequest(reqClone.url, {
...reqClone,
method: reqClone.method,
headers: reqClone.headers,
body: JSON.stringify(data)
});

reqClone.json = async () => data;
} catch {
return NextResponse.json(
{
Expand All @@ -150,7 +156,7 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
FORM_DATA_CONTENT_TYPES.includes(contentType as FormDataContentType)
) {
try {
const formData = await req.clone().formData();
const formData = await reqClone.formData();

const { valid, errors, data } = validateSchema({
schema: bodySchema,
Expand All @@ -171,7 +177,6 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(

// Inject parsed for data to JSON body.
reqClone = new NextRequest(reqClone.url, {
...reqClone,
method: reqClone.method,
headers: reqClone.headers,
body: JSON.stringify(data)
Expand Down Expand Up @@ -203,7 +208,9 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
if (querySchema) {
const { valid, errors, data } = validateSchema({
schema: querySchema,
obj: qs.parse(req.nextUrl.search, { ignoreQueryPrefix: true })
obj: qs.parse(reqClone.nextUrl.search, {
ignoreQueryPrefix: true
})
});

if (!valid) {
Expand All @@ -230,9 +237,38 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
});

reqClone = new NextRequest(url, {
...reqClone,
method: reqClone.method,
headers: reqClone.headers
headers: reqClone.headers,
body: reqClone.body
});
}

if (paramsSchema) {
const { valid, errors, data } = validateSchema({
schema: paramsSchema,
obj: context.params
});

if (!valid) {
return NextResponse.json(
{
message: DEFAULT_ERRORS.invalidPathParameters,
errors
},
{
status: 400
}
);
}

context.params = data;
const url = new URL(reqClone.url);
url.search = new URLSearchParams(context.params).toString();

reqClone = new NextRequest(url, {
method: reqClone.method,
headers: reqClone.headers,
body: reqClone.body
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/next-rest-framework/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export const DEFAULT_ERRORS = {
operationNotAllowed: 'Operation not allowed.',
invalidRequestBody: 'Invalid request body.',
missingRequestBody: 'Missing request body.',
invalidQueryParameters: 'Invalid query parameters.'
invalidQueryParameters: 'Invalid query parameters.',
invalidPathParameters: 'Invalid path parameters.'
};

export enum ValidMethod {
Expand Down

0 comments on commit 1dc6506

Please sign in to comment.