Skip to content

Commit

Permalink
Merge branch 'master' into make-v23
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinTail authored Feb 3, 2025
2 parents 1c8b678 + a18d500 commit 45d9fd1
Show file tree
Hide file tree
Showing 15 changed files with 590 additions and 351 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@

## Version 22

### v22.5.0

- Feature: Ability to respond with status code `405` (Method not allowed) to requests having wrong method:
- Previously, in all cases where the method and route combination was not defined, the response had status code `404`;
- For situations where a known route does not support the method being used, there is a more appropriate code `405`:
- See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 for details;
- You can activate this feature by setting the new `wrongMethodBehavior` config option `405` (default: `404`).

```ts
import { createConfig } from "express-zod-api";

createConfig({ wrongMethodBehavior: 405 });
```

### v22.4.2

- Excluded 41 response-only headers from the list of well-known ones used to depict request params in Documentation.

### v22.4.1

- Fixed a bug that could lead to duplicate properties in generated client types:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,8 @@ origins of errors that could happen in runtime and be handled the following way:
- Others, inheriting from `Error` class (`500`);
- Ones related to routing, parsing and upload issues — handled by `ResultHandler` assigned to `errorHandler` in config:
- Default is `defaultResultHandler` — it sets the response status code from the corresponding `HttpError`:
`400` for parsing, `404` for routing, `config.upload.limitError.statusCode` for upload issues, or `500` for others.
`400` for parsing, `404` for routing, `config.upload.limitError.statusCode` for upload issues, or `500` for others;
- You can also set `wrongMethodBehavior` config option to `405` (Method not allowed) for requests having wrong method;
- `ResultHandler` must handle possible `error` and avoid throwing its own errors, otherwise:
- Ones related to `ResultHandler` execution — handled by `LastResortHandler`:
- Response status code is always `500` and the response itself is a plain text.
Expand Down
8 changes: 8 additions & 0 deletions src/config-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export interface CommonConfig {
* @desc You can override the default CORS headers by setting up a provider function here.
*/
cors: boolean | HeadersProvider;
/**
* @desc How to respond to a request that uses a wrong method to an existing endpoint
* @example 404 — Not found
* @example 405 — Method not allowed, incl. the "Allowed" header with a list of methods
* @default 404
* @todo consider changing default to 405 in v23
* */
wrongMethodBehavior?: 404 | 405;
/**
* @desc The ResultHandler to use for handling routing, parsing and upload errors
* @default defaultResultHandler
Expand Down
11 changes: 7 additions & 4 deletions src/result-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,13 @@ export const defaultResultHandler = new ResultHandler({
if (error) {
const httpError = ensureHttpError(error);
logServerError(httpError, logger, request, input);
return void response.status(httpError.statusCode).json({
status: "error",
error: { message: getPublicErrorMessage(httpError) },
});
return void response
.status(httpError.statusCode)
.set(httpError.headers)
.json({
status: "error",
error: { message: getPublicErrorMessage(httpError) },
});
}
response
.status(defaultStatusCodes.positive)
Expand Down
28 changes: 24 additions & 4 deletions src/routing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IRouter, RequestHandler } from "express";
import createHttpError from "http-errors";
import { isProduction } from "./common-helpers";
import { CommonConfig } from "./config-type";
import { ContentType } from "./content-type";
Expand All @@ -16,6 +17,18 @@ export interface Routing {

export type Parsers = Partial<Record<ContentType, RequestHandler[]>>;

/** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 */
export const createWrongMethodHandler =
(allowedMethods: Array<Method | AuxMethod>): RequestHandler =>
({ method }, res, next) => {
const Allow = allowedMethods.join(", ").toUpperCase();
res.set({ Allow }); // in case of a custom errorHandler configured that does not care about headers in error
const error = createHttpError(405, `${method} is not allowed`, {
headers: { Allow },
});
next(error);
};

export const initRouting = ({
app,
getLogger,
Expand All @@ -30,7 +43,7 @@ export const initRouting = ({
parsers?: Parsers;
}) => {
const doc = new Diagnostics(getLogger());
const corsedPaths = new Set<string>();
const familiar = new Map<string, Array<Method | AuxMethod>>();
const onEndpoint: OnEndpoint = (endpoint, path, method, siblingMethods) => {
if (!isProduction()) doc.check(endpoint, { path, method });
const matchingParsers = parsers?.[endpoint.getRequestType()] || [];
Expand All @@ -56,11 +69,18 @@ export const initRouting = ({
}
return endpoint.execute({ request, response, logger, config });
};
if (config.cors && !corsedPaths.has(path)) {
app.options(path, ...matchingParsers, handler);
corsedPaths.add(path);
if (!familiar.has(path)) {
familiar.set(path, []);
if (config.cors) {
app.options(path, ...matchingParsers, handler);
familiar.get(path)?.push("options");
}
}
familiar.get(path)?.push(method);
app[method](path, ...matchingParsers, handler);
};
walkRouting({ routing, onEndpoint, onStatic: app.use.bind(app) });
if (config.wrongMethodBehavior !== 405) return;
for (const [path, allowedMethods] of familiar.entries())
app.all(path, createWrongMethodHandler(allowedMethods));
};
2 changes: 1 addition & 1 deletion src/server-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface HandlerCreatorParams {
getLogger: GetLogger;
}

export const createParserFailureHandler =
export const createCatcher =
({ errorHandler, getLogger }: HandlerCreatorParams): ErrorRequestHandler =>
async (error, request, response, next) => {
if (!error) return next();
Expand Down
17 changes: 6 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Parsers, Routing, initRouting } from "./routing";
import {
createLoggingMiddleware,
createNotFoundHandler,
createParserFailureHandler,
createCatcher,
createUploadParsers,
makeGetLogger,
installDeprecationListener,
Expand All @@ -40,12 +40,12 @@ const makeCommonEntities = (config: CommonConfig) => {
const getLogger = makeGetLogger(logger);
const commons = { getLogger, errorHandler };
const notFoundHandler = createNotFoundHandler(commons);
const parserFailureHandler = createParserFailureHandler(commons);
const catcher = createCatcher(commons);
return {
...commons,
logger,
notFoundHandler,
parserFailureHandler,
catcher,
loggingMiddleware,
};
};
Expand All @@ -63,13 +63,8 @@ export const attachRouting = (config: AppConfig, routing: Routing) => {
};

export const createServer = async (config: ServerConfig, routing: Routing) => {
const {
logger,
getLogger,
notFoundHandler,
parserFailureHandler,
loggingMiddleware,
} = makeCommonEntities(config);
const { logger, getLogger, notFoundHandler, catcher, loggingMiddleware } =
makeCommonEntities(config);
const app = express().disable("x-powered-by").use(loggingMiddleware);

if (config.compression) {
Expand All @@ -91,7 +86,7 @@ export const createServer = async (config: ServerConfig, routing: Routing) => {

await config.beforeRouting?.({ app, getLogger });
initRouting({ app, routing, getLogger, config, parsers });
app.use(parserFailureHandler, notFoundHandler);
app.use(catcher, notFoundHandler);

const created: Array<http.Server | https.Server> = [];
const makeStarter =
Expand Down
41 changes: 0 additions & 41 deletions src/well-known-headers.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,11 @@
"accept-encoding",
"accept-features",
"accept-language",
"accept-patch",
"accept-post",
"accept-ranges",
"accept-signature",
"access-control",
"access-control-allow-credentials",
"access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin",
"access-control-expose-headers",
"access-control-max-age",
"access-control-request-headers",
"access-control-request-method",
"age",
"allow",
"alpn",
"alt-svc",
"alt-used",
"alternates",
"amp-cache-transform",
Expand All @@ -39,15 +27,11 @@
"c-pep",
"c-pep-info",
"cache-control",
"cache-status",
"cal-managed-id",
"caldav-timezones",
"capsule-protocol",
"cdn-cache-control",
"cdn-loop",
"cert-not-after",
"cert-not-before",
"clear-site-data",
"client-cert",
"client-cert-chain",
"close",
Expand All @@ -60,7 +44,6 @@
"concealed-auth-export",
"configuration-context",
"connection",
"content-base",
"content-digest",
"content-disposition",
"content-encoding",
Expand All @@ -71,11 +54,7 @@
"content-md5",
"content-range",
"content-script-type",
"content-security-policy",
"content-security-policy-report-only",
"content-style-type",
"content-type",
"content-version",
"cookie",
"cookie2",
"cross-origin-embedder-policy",
Expand All @@ -100,10 +79,8 @@
"dpop-nonce",
"early-data",
"ediint-features",
"etag",
"expect",
"expect-ct",
"expires",
"ext",
"forwarded",
"from",
Expand All @@ -124,10 +101,8 @@
"keep-alive",
"label",
"last-event-id",
"last-modified",
"link",
"link-template",
"location",
"lock-token",
"man",
"max-forwards",
Expand All @@ -143,7 +118,6 @@
"odata-maxversion",
"odata-version",
"opt",
"optional-www-authenticate",
"ordering-type",
"origin",
"origin-agent-cluster",
Expand All @@ -167,12 +141,9 @@
"protocol-info",
"protocol-query",
"protocol-request",
"proxy-authenticate",
"proxy-authentication-info",
"proxy-authorization",
"proxy-features",
"proxy-instruction",
"proxy-status",
"public",
"public-key-pins",
"public-key-pins-report-only",
Expand All @@ -181,45 +152,35 @@
"referer",
"referer-root",
"referrer-policy",
"refresh",
"repeatability-client-id",
"repeatability-first-sent",
"repeatability-request-id",
"repeatability-result",
"replay-nonce",
"reporting-endpoints",
"repr-digest",
"retry-after",
"safe",
"schedule-reply",
"schedule-tag",
"sec-gpc",
"sec-purpose",
"sec-token-binding",
"sec-websocket-accept",
"sec-websocket-extensions",
"sec-websocket-key",
"sec-websocket-protocol",
"sec-websocket-version",
"security-scheme",
"server",
"server-timing",
"set-cookie",
"set-cookie2",
"setprofile",
"signature",
"signature-input",
"slug",
"soapaction",
"status-uri",
"strict-transport-security",
"sunset",
"surrogate-capability",
"surrogate-control",
"tcn",
"te",
"timeout",
"timing-allow-origin",
"topic",
"traceparent",
"tracestate",
Expand All @@ -232,13 +193,11 @@
"use-as-dictionary",
"user-agent",
"variant-vary",
"vary",
"via",
"want-content-digest",
"want-digest",
"want-repr-digest",
"warning",
"www-authenticate",
"x-content-type-options",
"x-frame-options"
]
1 change: 1 addition & 0 deletions tests/express-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const appMock = {
delete: vi.fn(),
options: vi.fn(),
init: vi.fn(),
all: vi.fn(),
};

const expressMock = () => appMock;
Expand Down
11 changes: 10 additions & 1 deletion tests/system/__snapshots__/system.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,16 @@ exports[`App in production mode > Positive > Should compress the response in cas
exports[`App in production mode > Protocol > Should fail on invalid method 1`] = `
{
"error": {
"message": "Can not PUT /v1/test",
"message": "PUT is not allowed",
},
"status": "error",
}
`;

exports[`App in production mode > Protocol > Should fail on invalid path 1`] = `
{
"error": {
"message": "Can not GET /v1/wrong",
},
"status": "error",
}
Expand Down
Loading

0 comments on commit 45d9fd1

Please sign in to comment.