Skip to content

Commit

Permalink
feat: move content-type check to body parser
Browse files Browse the repository at this point in the history
This allows for the 404 checks to happen before validating the media type.
  • Loading branch information
DASPRiD committed Mar 4, 2024
1 parent 53b313c commit e7180b6
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 70 deletions.
70 changes: 3 additions & 67 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
import contentTypeUtil from "content-type";
import { isHttpError } from "http-errors";
import type { Context, Middleware, ParameterizedContext } from "koa";
import type { Middleware, ParameterizedContext } from "koa";
import { type JsonApiMediaType, getAcceptableMediaTypes } from "./accept.js";
import { JsonApiBody, JsonApiErrorBody } from "./body.js";
import { InputValidationError } from "./request.js";

type RequestMiddlewareOptions = {
/**
* List of paths to exclude, supports non-greedy wildcards (*) in any position
*/
excludedPaths?: string[];

/**
* List of acceptable media type extensions
*/
acceptableExtensions?: string[];
};

type JsonApiState = {
jsonApi: {
acceptableTypes: JsonApiMediaType[];
};
};

export const jsonApiRequestMiddleware = (
options?: RequestMiddlewareOptions,
): Middleware<JsonApiState> => {
const excludeRegexp = options?.excludedPaths ? buildExcludeRegExp(options.excludedPaths) : null;

export const jsonApiRequestMiddleware = (): Middleware<JsonApiState> => {
return async (context, next) => {
context.state.jsonApi = {
acceptableTypes: getAcceptableMediaTypes(context.get("Accept")),
};

if (!excludeRegexp?.test(context.path) || validateContentType(context)) {
await next();
}

await next();
handleResponse(context);
};
};
Expand Down Expand Up @@ -84,44 +65,6 @@ export const jsonApiErrorMiddleware = (options?: ErrorMiddlewareOptions): Middle
};
};

const validateContentType = (context: Context): boolean => {
const contentType = context.request.get("Content-Type");

if (contentType === "") {
return true;
}

const parts = contentTypeUtil.parse(contentType);

if (parts.type !== "application/vnd.api+json") {
context.status = 415;
context.body = new JsonApiErrorBody({
status: "415",
code: "unsupported_media_type",
title: "Unsupported Media Type",
detail: `Unsupported media type '${parts.type}', use 'application/vnd.api+json'`,
});

return false;
}

const { ext, profile, ...rest } = parts.parameters;

if (Object.keys(rest).length > 0) {
context.status = 415;
context.body = new JsonApiErrorBody({
status: "415",
code: "unsupported_media_type",
title: "Unsupported Media Type",
detail: `Unknown media type parameters: ${Object.keys(rest).join(", ")}`,
});

return false;
}

return true;
};

const handleResponse = (context: ParameterizedContext<JsonApiState>): void => {
if (!(context.body instanceof JsonApiBody)) {
return;
Expand Down Expand Up @@ -174,10 +117,3 @@ const handleResponse = (context: ParameterizedContext<JsonApiState>): void => {
}),
);
};

const buildExcludeRegExp = (excludedPaths: string[]): RegExp => {
const regExpParts = excludedPaths.map((path) =>
path.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace(/\*/g, ".*?"),
);
return new RegExp(`^(?:${regExpParts.join("|")})$`);
};
52 changes: 49 additions & 3 deletions src/request.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contentTypeUtil from "content-type";
import type { Context } from "koa";
import qs from "qs";
import z from "zod";
Expand Down Expand Up @@ -101,14 +102,14 @@ const fixedIdSchema = <TId extends string>(id: TId) =>
}),
) as unknown as z.ZodType<TId>;

const fixedTypeSchema = <TType extends string>(id: TType) =>
const fixedTypeSchema = <TType extends string>(type: TType) =>
z.string().refine(
(value) => value === id,
(value) => value === type,
(value) => ({
message: "Type mismatch",
params: new JsonApiZodErrorParams(
"type_mismatch",
`Type '${value}' does not match '${id}'`,
`Type '${value}' does not match '${type}'`,
409,
),
}),
Expand Down Expand Up @@ -167,6 +168,49 @@ type ParseDataRequestResult<
: undefined;
};

const validateContentType = (context: Context): void => {
const contentType = context.request.get("Content-Type");

if (contentType === "") {
throw new InputValidationError("Unsupported Media Type", [
{
status: "415",
code: "unsupported_media_type",
title: "Unsupported Media Type",
detail: `Media type is missing, use 'application/vnd.api+json'`,
},
]);
}

const parts = contentTypeUtil.parse(contentType);

if (parts.type !== "application/vnd.api+json") {
throw new InputValidationError("Unsupported Media Type", [
{
status: "415",
code: "unsupported_media_type",
title: "Unsupported Media Type",
detail: `Unsupported media type '${parts.type}', use 'application/vnd.api+json'`,
},
]);
}

const { ext, profile, ...rest } = parts.parameters;

if (Object.keys(rest).length === 0) {
return;
}

throw new InputValidationError("Unsupported Media Type", [
{
status: "415",
code: "unsupported_media_type",
title: "Unsupported Media Type",
detail: `Unknown media type parameters: ${Object.keys(rest).join(", ")}`,
},
]);
};

const parseDataRequest = <
TIdSchema extends z.ZodType<unknown>,
TType extends string,
Expand All @@ -177,6 +221,8 @@ const parseDataRequest = <
koaContext: Context,
options: ParseDataRequestOptions<TType, TAttributesSchema, TRelationshipsSchema>,
): ParseDataRequestResult<TIdSchema, TType, TAttributesSchema, TRelationshipsSchema> => {
validateContentType(koaContext);

const parseResult = z
.object({
data: z.object({
Expand Down

0 comments on commit e7180b6

Please sign in to comment.