diff --git a/server/package.json b/server/package.json index 187b33d..b00d445 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@ty-ras/server", - "version": "2.2.0", + "version": "2.2.1", "author": { "name": "Stanislav Muhametsin", "email": "346799+stazz@users.noreply.github.com", diff --git a/server/src/__test__/flow.spec.ts b/server/src/__test__/flow.spec.ts index 0af968e..59d6ece 100644 --- a/server/src/__test__/flow.spec.ts +++ b/server/src/__test__/flow.spec.ts @@ -1346,6 +1346,116 @@ test("Validate typicalServerFlow works when sendBody throws inside error handler ]); }); +test("Validate that handling OPTIONS works as intended by typicalServeFlow when invalid method returned", async (c) => { + c.plan(1); + const { seenCallbacks, callbacks } = flowUtil.customizeTrackingCallback({ + getMethod: () => "OPTIONS", + }); + const allowedMethods = ["GET", "POST"] as const; + await flowUtil.createTypicalServerFlow( + { + url: /(?\/path)/, + handler: () => ({ + found: "invalid-method", + allowedMethods: allowedMethods.map((method) => ({ + method, + stateInformation: flowUtil.createStateValidator(), + })), + }), + }, + callbacks, + undefined, + )(flowUtil.inputContext); + c.deepEqual(seenCallbacks, [ + { + args: [flowUtil.seenContext], + callbackName: "getURL", + returnValue: "/path", + }, + { + args: [flowUtil.seenContext], + callbackName: "getMethod", + returnValue: "OPTIONS", + }, + { + args: [flowUtil.seenContext, "Allow", allowedMethods.join(",")], + callbackName: "setHeader", + returnValue: undefined, + }, + { + args: [flowUtil.seenContext, 200, false], + callbackName: "setStatusCode", + returnValue: undefined, + }, + ]); +}); + +test("Validate that handling OPTIONS works as intended by typicalServeFlow when no allowed methods returned", async (c) => { + c.plan(1); + const { seenCallbacks, callbacks } = flowUtil.customizeTrackingCallback({ + getMethod: () => "OPTIONS", + }); + await flowUtil.createTypicalServerFlow( + { + url: /(?\/path)/, + handler: () => ({ + found: "invalid-method", + allowedMethods: [], + }), + }, + callbacks, + undefined, + )(flowUtil.inputContext); + c.deepEqual(seenCallbacks, [ + { + args: [flowUtil.seenContext], + callbackName: "getURL", + returnValue: "/path", + }, + { + args: [flowUtil.seenContext], + callbackName: "getMethod", + returnValue: "OPTIONS", + }, + { + args: [flowUtil.seenContext, 404, false], + callbackName: "setStatusCode", + returnValue: undefined, + }, + ]); +}); + +test("Validate that handling OPTIONS works as intended by typicalServeFlow when no handlers returned", async (c) => { + c.plan(1); + const { seenCallbacks, callbacks } = flowUtil.customizeTrackingCallback({ + getURL: () => "/will-not-match", + getMethod: () => "OPTIONS", + }); + await flowUtil.createTypicalServerFlow( + { + url: /(?\/path)/, + handler: () => ({ + found: "invalid-method", + allowedMethods: [], + }), + }, + callbacks, + undefined, + )(flowUtil.inputContext); + c.deepEqual(seenCallbacks, [ + { + args: [flowUtil.seenContext], + callbackName: "getURL", + returnValue: "/will-not-match", + }, + { + args: [flowUtil.seenContext, 404, false], + callbackName: "setStatusCode", + returnValue: undefined, + }, + ]); +}); + const getHumanReadableMessage = () => ""; const errorMessage = "This should never be called"; diff --git a/server/src/flow.ts b/server/src/flow.ts index 0208b18..bffc36d 100644 --- a/server/src/flow.ts +++ b/server/src/flow.ts @@ -3,7 +3,7 @@ */ import type * as ep from "@ty-ras/endpoint"; -import type * as protocol from "@ty-ras/protocol"; +import * as protocol from "@ty-ras/protocol"; import * as data from "@ty-ras/data"; import * as dataBE from "@ty-ras/data-backend"; import * as server from "./utils"; @@ -17,7 +17,7 @@ import * as stream from "node:stream"; * Creates a callback which will asynchronously process each incoming HTTP request, to extract the endpoint based on URL path and method, validate all necessary inputs, invoke the endpoint, validate all necesary outputs, and write the result to HTTP response. * This function is used by other TyRAS plugins and usually not directly by client code. * @param endpoints All the {@link ep.AppEndpoint}s to use in returned callback. - * @param callbacks The {@see ServerFlowCallbacks} necessary to actually implement the returned callback. + * @param callbacks The {@link ServerFlowCallbacks} necessary to actually implement the returned callback. * @param events The {@link evt.ServerEventHandler} to invoke on events. * @returns The callback which can be used to asynchronously process incoming HTTP request, and write to outgoing HTTP response. */ @@ -70,13 +70,15 @@ export const createTypicalServerFlow = < // We have a match -> get the handler that will handle our match const method = cb.getMethod(ctx) as protocol.HttpMethod; let foundHandler = handler(method, maybeEventArgs.groups); - const sendBody = method !== "HEAD"; + const sendBody = method !== protocol.METHOD_HEAD; if ( foundHandler.found !== "handler" && !sendBody && - foundHandler.allowedMethods.some(({ method }) => method === "GET") + foundHandler.allowedMethods.some( + ({ method }) => method === protocol.METHOD_GET, + ) ) { - foundHandler = handler("GET", maybeEventArgs.groups); + foundHandler = handler(protocol.METHOD_GET, maybeEventArgs.groups); } if (foundHandler.found === "handler") { @@ -244,7 +246,7 @@ export const createTypicalServerFlow = < maybeEventArgs, events, foundHandler.allowedMethods, - method === "OPTIONS" + method === protocol.METHOD_OPTIONS ? undefined : async (stateValidator) => stateValidator.validator( @@ -254,8 +256,12 @@ export const createTypicalServerFlow = < if (!ctx.skipSettingStatusCode) { const statusCode = - allowedMethodsSentToClient.length > 0 ? 405 : 404; - if (statusCode === 405) { + allowedMethodsSentToClient.length > 0 + ? method === protocol.METHOD_OPTIONS + ? 200 + : 405 + : 404; + if (statusCode !== 404) { cb.setHeader(ctx, "Allow", allowedMethodsSentToClient.join(",")); } cb.setStatusCode(ctx, statusCode, false); diff --git a/server/src/utils.ts b/server/src/utils.ts index 2fac2e0..43413f6 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -29,7 +29,6 @@ export const checkURLPathNameForHandler = ( ): evt.EventArgumentsWithoutState | undefined => { const pathName = (url instanceof u.URL ? url : new u.URL(url)).pathname; const groups = regExp.exec(pathName)?.groups; - // console.log("LEL", regExp, pathName, groups); if (!groups) { events?.("onInvalidUrl", { ctx,