Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/runtime-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ on:
workflow_dispatch

jobs:

test-bun:
name: Test on Bun
runs-on: ubuntu-latest
Expand All @@ -21,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18.x', '20.x']
node-version: ["18.x", "20.x"]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
Expand All @@ -33,13 +32,13 @@ jobs:
npm install
- name: Perform Tests
run: npm test

test-cloudflare-workers:
name: Test on Cloudflare Workers dev env
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18.x', '20.x']
node-version: ["18.x", "20.x"]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## [0.12.0] - 2024-11-05

### Changed

- lax parsing rule for GET requests with header 'content-type: application/json'
- upgraded dependencies (`@std/path@^1.0.8`, `@std/testing@^1.0.4`,
`@std/assert@^1.0.7`, `@oak/oak@^17.1.3`,
`@asteasolutions/zod-to-openapi@^7.2.0`, `@std/io@^0.225.0`)
- code format

## [0.11.0] - 2024-09-11

### Changed
Expand Down
12 changes: 6 additions & 6 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
export { join } from "jsr:@std/path@^1.0.4";
export { join } from "jsr:@std/path@^1.0.8";

export { Router, Status } from "jsr:@oak/oak@^17.0.0";
export { Router, Status } from "jsr:@oak/oak@^17.1.3";

export type {
Application,
Context,
ErrorStatus,
Next,
RouteContext,
} from "jsr:@oak/oak@^17.0.0";
} from "jsr:@oak/oak@^17.1.3";

import {
extendZodWithOpenApi,
type ResponseConfig,
type RouteConfig,
} from "npm:@asteasolutions/zod-to-openapi@^7.1.1";
} from "npm:@asteasolutions/zod-to-openapi@^7.2.0";

export type OakOpenApiSpec =
& Omit<RouteConfig, "method" | "path" | "responses">
Expand All @@ -30,9 +30,9 @@ export {
OpenApiGeneratorV3,
OpenAPIRegistry,
type ZodRequestBody,
} from "npm:@asteasolutions/zod-to-openapi@^7.1.1";
} from "npm:@asteasolutions/zod-to-openapi@^7.2.0";

export { type OpenAPIObjectConfig } from "npm:@asteasolutions/zod-to-openapi@^7.1.1/dist/v3.0/openapi-generator";
export { type OpenAPIObjectConfig } from "npm:@asteasolutions/zod-to-openapi@^7.2.0/dist/v3.0/openapi-generator";

// must import from `npm:` instead of from `deno.land` to be compatible with `@asteasolutions/zod-to-openapi`
import { z as slowTypedZ } from "npm:zod@^3.23.8";
Expand Down
14 changes: 7 additions & 7 deletions dev_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ export {
assertObjectMatch,
assertStringIncludes,
assertThrows,
} from "jsr:@std/assert@^1.0.4";
} from "jsr:@std/assert@^1.0.7";

export {
type BodyType,
type Middleware,
Request,
testing as oakTesting,
} from "jsr:@oak/oak@^17.0.0";
} from "jsr:@oak/oak@^17.1.3";

export { Body } from "jsr:@oak/oak@^17.0.0/body";
export { Body } from "jsr:@oak/oak@^17.1.3/body";

export {
assertSpyCall,
Expand All @@ -25,17 +25,17 @@ export {
spy,
type Stub,
stub,
} from "jsr:@std/testing@^1.0.2/mock";
} from "jsr:@std/testing@^1.0.4/mock";

export { assertSnapshot } from "jsr:@std/testing@^1.0.2/snapshot";
export { assertSnapshot } from "jsr:@std/testing@^1.0.4/snapshot";

export { Buffer } from "jsr:@std/io@^0.224.7";
export { Buffer } from "jsr:@std/io@^0.225.0";

export {
afterEach,
beforeEach,
describe,
it,
} from "jsr:@std/testing@^1.0.2/bdd";
} from "jsr:@std/testing@^1.0.4/bdd";

export { ZodObject } from "npm:zod@^3.23.8";
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dklab/oak-routing-ctrl",
"version": "0.11.0",
"version": "0.12.0",
"exports": {
".": "./mod.ts",
"./mod": "./mod.ts"
Expand Down
26 changes: 18 additions & 8 deletions src/ControllerMethodArgs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { debug } from "./utils/logger.ts";
import { Context, RouteContext } from "../deps.ts";
import { type Context, type RouteContext } from "../deps.ts";
import { ERR_UNSUPPORTED_CLASS_METHOD_DECORATOR_RUNTIME_BEHAVIOR } from "./Constants.ts";

/**
Expand Down Expand Up @@ -128,13 +128,23 @@ function getEnhancedHandler(
try {
parsedReqBody = await _internal.parseOakReqBody(ctx);
} catch (e) {
return ctx.throw(
400,
`Unable to parse request body: ${(e as Error).message}`,
{
stack: (e as Error).stack,
},
);
if (
ctx.request.method === "GET" &&
ctx.request.headers.get("Content-Type") === "application/json" &&
(e as Error).message?.includes("Unexpected end of JSON input")
) {
// we ignore this parsing error because the client was sending
// a weird combination of method & content-type header
} else {
// for other case, we trigger the error back to userland
return ctx.throw(
400,
`Unable to parse request body: ${(e as Error).message}`,
{
stack: (e as Error).stack,
},
);
}
}

const parsedReqSearchParams: Record<string, string> = {};
Expand Down
77 changes: 65 additions & 12 deletions src/ControllerMethodArgs_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,33 @@ Deno.test("getEnhancedHandler with a faulty ctx.request.body", async () => {
spyParseOakRequestBody.restore();
});

Deno.test("getEnhancedHandler with a faulty request method and content-type combination", async () => {
const spyParseOakRequestBody = spy(_internal, "parseOakReqBody");
function testHandler() {
return "weird.method.content-type.combination.handled";
}
// deno-lint-ignore ban-types
const enhancedHandler: Function = _internal.getEnhancedHandler(testHandler);
const ctx = createMockContext({
method: "GET",
headers: [["Content-Type", "application/json"]],
});
Object.defineProperty(ctx.request, "body", {
get: () => createMockRequestBody("json", "Unexpected end of JSON input"),
});
const spyCtxThrow = spy();
Object.defineProperty(ctx, "throw", {
value: (errorStatus: unknown, message?: string, props?: unknown) => {
spyCtxThrow(errorStatus, message, props);
},
});
const retVal = await enhancedHandler(ctx);
assertSpyCalls(spyCtxThrow, 0);
assertSpyCalls(spyParseOakRequestBody, 1);
assertEquals(retVal, "weird.method.content-type.combination.handled");
spyParseOakRequestBody.restore();
});

Deno.test("getEnhancedHandler with a faulty ctx.request.url.searchParams", async () => {
const spyParseOakRequestBody = spy(_internal, "parseOakReqBody");
function testHandler() {
Expand Down Expand Up @@ -669,7 +696,8 @@ Deno.test("getEnhancedHandler with a faulty ctx.request.url.searchParams", async
});

Deno.test("getEnhancedHandler - declaring 4 desirable params in order A", async () => {
const testHandler = spy((..._rest) => 42);
// deno-lint-ignore no-explicit-any
const testHandler = spy((..._rest: any[]) => 42);
// deno-lint-ignore ban-types
const enhancedHandler: Function = _internal.getEnhancedHandler(
testHandler,
Expand Down Expand Up @@ -699,7 +727,8 @@ Deno.test("getEnhancedHandler - declaring 4 desirable params in order A", async
});

Deno.test("getEnhancedHandler - declaring 4 desirable params in order B", async () => {
const testHandler = spy((..._rest) => 43);
// deno-lint-ignore no-explicit-any
const testHandler = spy((..._rest: any[]) => 43);
// deno-lint-ignore ban-types
const enhancedHandler: Function = _internal.getEnhancedHandler(
testHandler,
Expand Down Expand Up @@ -730,7 +759,8 @@ Deno.test("getEnhancedHandler - declaring 4 desirable params in order B", async

Deno.test("getEnhancedHandler - declaring 5 desirable params", async () => {
const spyParseOakRequestBody = spy(_internal, "parseOakReqBody");
const testHandler = spy((..._rest) => 44);
// deno-lint-ignore no-explicit-any
const testHandler = spy((..._rest: any[]) => 44);
// deno-lint-ignore ban-types
const enhancedHandler: Function = _internal.getEnhancedHandler(
testHandler,
Expand Down Expand Up @@ -765,7 +795,8 @@ Deno.test("getEnhancedHandler - declaring 5 desirable params", async () => {
});

Deno.test("getEnhancedHandler - declaring 3 desirable params in order A", async () => {
const testHandler = spy((..._rest) => 45);
// deno-lint-ignore no-explicit-any
const testHandler = spy((..._rest: any[]) => 45);
// deno-lint-ignore ban-types
const enhancedHandler: Function = _internal.getEnhancedHandler(
testHandler,
Expand Down Expand Up @@ -794,7 +825,8 @@ Deno.test("getEnhancedHandler - declaring 3 desirable params in order A", async
});

Deno.test("getEnhancedHandler - declaring 3 desirable params in order B", async () => {
const testHandler = spy((..._rest) => 46);
// deno-lint-ignore no-explicit-any
const testHandler = spy((..._rest: any[]) => 46);
// deno-lint-ignore ban-types
const enhancedHandler: Function = _internal.getEnhancedHandler(
testHandler,
Expand Down Expand Up @@ -847,7 +879,10 @@ Deno.test("getEnhancedHandler - not declaring any param", async () => {
/**
* @NOTE if/when `oak` supports such a method, better import from there instead
*/
function createMockRequestBody(type: BodyType): Body {
function createMockRequestBody(
type: BodyType,
thrownMsgWhenParsed?: string,
): Body {
const buf = new Buffer();
const rs = new ReadableStream();
const retVal = new Body({
Expand All @@ -865,22 +900,40 @@ function createMockRequestBody(type: BodyType): Body {
value: () => type,
},
json: {
value: () => Promise.resolve({ mock: "mock" }),
value: () =>
!thrownMsgWhenParsed
? Promise.resolve({ mock: "mock" })
: Promise.reject(new Error(thrownMsgWhenParsed)),
},
text: {
value: () => Promise.resolve("mock"),
value: () =>
!thrownMsgWhenParsed
? Promise.resolve("mock")
: Promise.reject(new Error(thrownMsgWhenParsed)),
},
blob: {
value: () => Promise.resolve(buf),
value: () =>
!thrownMsgWhenParsed
? Promise.resolve(buf)
: Promise.reject(new Error(thrownMsgWhenParsed)),
},
form: {
value: () => Promise.resolve(new URLSearchParams({ mock: "mock" })),
value: () =>
!thrownMsgWhenParsed
? Promise.resolve(new URLSearchParams({ mock: "mock" }))
: Promise.reject(new Error(thrownMsgWhenParsed)),
},
formData: {
value: () => Promise.resolve(new FormData()),
value: () =>
!thrownMsgWhenParsed
? Promise.resolve(new FormData())
: Promise.reject(new Error(thrownMsgWhenParsed)),
},
arrayBuffer: {
value: () => Promise.resolve(buf),
value: () =>
!thrownMsgWhenParsed
? Promise.resolve(buf)
: Promise.reject(new Error(thrownMsgWhenParsed)),
},
});
return retVal;
Expand Down