Skip to content

Commit e69a298

Browse files
fix(routing): match trailing-slash paths for Express compatibility
1 parent d358f73 commit e69a298

3 files changed

Lines changed: 35 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.1] - 2026-05-20
11+
12+
### Fixed
13+
- trailing slash routes return 404 #182
14+
1015
## [2.0.0] - 2026-05-20
1116

1217
### Added
@@ -149,6 +154,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
149154
- Exception handling with WsException class
150155
- Broadcast operator with room targeting and client exclusion
151156

152-
[Unreleased]: https://github.com/FOSSFORGE/uWestJS/compare/v1.0.1...HEAD
157+
[Unreleased]: https://github.com/FOSSFORGE/uWestJS/compare/v2.0.1...HEAD
158+
[2.0.1]: https://github.com/FOSSFORGE/uWestJS/compare/v2.0.0...v2.0.1
159+
[2.0.0]: https://github.com/FOSSFORGE/uWestJS/compare/v1.0.1...v2.0.0
153160
[1.0.1]: https://github.com/FOSSFORGE/uWestJS/compare/v1.0.0...v1.0.1
154161
[1.0.0]: https://github.com/FOSSFORGE/uWestJS/releases/tag/v1.0.0

src/http/routing/route-registry.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('RouteRegistry', () => {
9797
registry.register('GET', '/items/:id', getHandler);
9898
registry.register('HEAD', '/items/:id', headHandler);
9999

100-
expect(mockUwsApp.head).toHaveBeenCalledTimes(1);
100+
expect(mockUwsApp.head).toHaveBeenCalledTimes(2);
101101

102102
const route = registeredRoutes.get('HEAD:/items/:id');
103103
expect(route).toBeDefined();

src/http/routing/route-registry.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export interface RouteInfo {
8585
handler: RouteHandler; // Store the handler
8686
metadata?: RouteMetadata; // Middleware metadata
8787
implicitHead?: boolean; // Auto-registered HEAD fallback for GET routes
88+
trailingSlash?: boolean; // Auto-registered trailing-slash variant for Express compatibility
8889
}
8990

9091
/**
@@ -252,6 +253,11 @@ export class RouteRegistry {
252253
};
253254

254255
this.routes.set(routeKey, routeInfo);
256+
// Also update trailing-slash variant if it exists
257+
const slashRouteKey = `${normalizedMethod}:${path}/`;
258+
if (this.routes.has(slashRouteKey)) {
259+
this.routes.set(slashRouteKey, { ...routeInfo, trailingSlash: true });
260+
}
255261
if (isComplex) {
256262
const staticPrefix = this.extractStaticPrefix(path);
257263
const registrationPath = staticPrefix ? `${staticPrefix}/*` : '/*';
@@ -385,11 +391,9 @@ export class RouteRegistry {
385391
this.complexRoutesByWildcard.get(wildcardKey)!.push(routeInfo);
386392
} else {
387393
// Simple route - use native uWS routing
388-
uwsMethodFn.call(
389-
this.uwsApp,
390-
uwsPath,
391-
async (uwsRes: uWS.HttpResponse, uwsReq: uWS.HttpRequest) => {
392-
const activeRoute = this.routes.get(routeKey)!;
394+
const createHandler =
395+
(key: string) => async (uwsRes: uWS.HttpResponse, uwsReq: uWS.HttpRequest) => {
396+
const activeRoute = this.routes.get(key)!;
393397

394398
// Create request/response wrappers
395399
const req = new UwsRequest(uwsReq, uwsRes, paramNames);
@@ -414,8 +418,19 @@ export class RouteRegistry {
414418

415419
// Execute handler with error handling
416420
await this.executeHandler(activeRoute.handler, req, res, activeRoute.metadata);
421+
};
422+
423+
uwsMethodFn.call(this.uwsApp, uwsPath, createHandler(routeKey));
424+
425+
// Also register trailing-slash variant for Express compatibility
426+
// (e.g. /api and /api/ should both match the same route)
427+
if (!uwsPath.endsWith('/') && uwsPath !== '/') {
428+
const slashRouteKey = `${normalizedMethod}:${path}/`;
429+
if (!this.routes.has(slashRouteKey)) {
430+
this.routes.set(slashRouteKey, { ...routeInfo, trailingSlash: true });
431+
uwsMethodFn.call(this.uwsApp, uwsPath + '/', createHandler(slashRouteKey));
417432
}
418-
);
433+
}
419434
}
420435

421436
if (normalizedMethod === 'GET' && !implicitHead) {
@@ -935,7 +950,9 @@ export class RouteRegistry {
935950
* @returns Map of route keys to route information
936951
*/
937952
getRoutes(): Map<string, RouteInfo> {
938-
return new Map([...this.routes].filter(([, route]) => !route.implicitHead));
953+
return new Map(
954+
[...this.routes].filter(([, route]) => !route.implicitHead && !route.trailingSlash)
955+
);
939956
}
940957

941958
/**
@@ -957,7 +974,8 @@ export class RouteRegistry {
957974
* @returns Number of registered routes
958975
*/
959976
getRouteCount(): number {
960-
return [...this.routes.values()].filter((route) => !route.implicitHead).length;
977+
return [...this.routes.values()].filter((route) => !route.implicitHead && !route.trailingSlash)
978+
.length;
961979
}
962980

963981
private replaceComplexRoute(wildcardKey: string, routeInfo: RouteInfo): void {

0 commit comments

Comments
 (0)