From 82a8f65551eae7af0f85667feb34f1b700d8b044 Mon Sep 17 00:00:00 2001 From: Vishal Kumar Singh Date: Sat, 16 May 2026 14:10:32 +0530 Subject: [PATCH] feat(custom-routes): expose query params on request object Adds a query property to ParamsIncomingMessage that contains parsed query string parameters from the request URL. When a custom route handler receives a request like /greetings?name=you, the handler can access req.query.name. For repeated keys (/test?tags=a&tags=b), the value becomes an array instead of a single string. Fixes #2100 --- src/index.ts | 1 + src/receivers/HTTPReceiver.ts | 15 +++++++++++++-- src/receivers/ParamsIncomingMessage.ts | 9 +++++++++ src/receivers/SocketModeReceiver.ts | 17 ++++++++++++++--- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 52f1693dd..d9f8e0cbf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,6 +68,7 @@ export { RequestVerificationOptions, } from './receivers/HTTPModuleFunctions'; export type { HTTPReceiverOptions } from './receivers/HTTPReceiver'; +export type { ParamsIncomingMessage, QueryDictionary } from './receivers/ParamsIncomingMessage'; export { HTTPResponseAck } from './receivers/HTTPResponseAck'; export { defaultProcessEventErrorHandler, diff --git a/src/receivers/HTTPReceiver.ts b/src/receivers/HTTPReceiver.ts index 115554707..cead9cb2f 100644 --- a/src/receivers/HTTPReceiver.ts +++ b/src/receivers/HTTPReceiver.ts @@ -31,7 +31,7 @@ import type { BufferedIncomingMessage } from './BufferedIncomingMessage'; import { buildReceiverRoutes, type CustomRoute, type ReceiverRoutes } from './custom-routes'; import * as httpFunc from './HTTPModuleFunctions'; import { HTTPResponseAck } from './HTTPResponseAck'; -import type { ParamsIncomingMessage } from './ParamsIncomingMessage'; +import type { ParamsIncomingMessage, QueryDictionary } from './ParamsIncomingMessage'; import { verifyRedirectOpts } from './verify-redirect-opts'; // Option keys for tls.createServer() and tls.createSecureContext(), exclusive of those for http.createServer() @@ -413,7 +413,9 @@ export default class HTTPReceiver implements Receiver { const pathMatch = matchRegex(path); if (pathMatch && this.routes[route][method] !== undefined) { const params = pathMatch.params as ParamsDictionary; - const message: ParamsIncomingMessage = Object.assign(req, { params }); + const parsedUrl = new URL(req.url as string, 'http://localhost'); + const query = parseSearchParams(parsedUrl.searchParams); + const message: ParamsIncomingMessage = Object.assign(req, { params, query }); return this.routes[route][method](message, res); } } @@ -567,3 +569,12 @@ export default class HTTPReceiver implements Receiver { } } } + +function parseSearchParams(searchParams: URLSearchParams): QueryDictionary { + const query: QueryDictionary = {}; + for (const key of searchParams.keys()) { + const values = searchParams.getAll(key); + query[key] = values.length === 1 ? values[0] : values; + } + return query; +} diff --git a/src/receivers/ParamsIncomingMessage.ts b/src/receivers/ParamsIncomingMessage.ts index 96f7e505e..21279bd0b 100644 --- a/src/receivers/ParamsIncomingMessage.ts +++ b/src/receivers/ParamsIncomingMessage.ts @@ -1,6 +1,8 @@ import type { IncomingMessage } from 'node:http'; import type { ParamsDictionary } from 'express-serve-static-core'; +export type QueryDictionary = Record; + export interface ParamsIncomingMessage extends IncomingMessage { /** * **Only valid for requests with path parameters.** @@ -10,4 +12,11 @@ export interface ParamsIncomingMessage extends IncomingMessage { * then `request.params` will be `{ id: '123' }`. */ params?: ParamsDictionary; + + /** + * The query parameters of the request. For example, if the request URL is + * `/greetings?name=you&tags=a&tags=b`, then `request.query` will be + * `{ name: 'you', tags: ['a', 'b'] }`. + */ + query?: QueryDictionary; } diff --git a/src/receivers/SocketModeReceiver.ts b/src/receivers/SocketModeReceiver.ts index 0d22078c0..e7e678250 100644 --- a/src/receivers/SocketModeReceiver.ts +++ b/src/receivers/SocketModeReceiver.ts @@ -17,7 +17,7 @@ import type { CodedError } from '../errors'; import type { Receiver, ReceiverEvent } from '../types'; import type { StringIndexed } from '../types/utilities'; import { buildReceiverRoutes, type ReceiverRoutes } from './custom-routes'; -import type { ParamsIncomingMessage } from './ParamsIncomingMessage'; +import type { ParamsIncomingMessage, QueryDictionary } from './ParamsIncomingMessage'; import { defaultProcessEventErrorHandler, type SocketModeReceiverProcessEventErrorHandlerArgs, @@ -209,7 +209,8 @@ export default class SocketModeReceiver implements Receiver { if (customRoutes.length && req.url) { // NOTE: the domain and scheme are irrelevant here. // The URL object is only used to safely obtain the path to match - const { pathname: path } = new URL(req.url as string, 'http://localhost'); + const parsedUrl = new URL(req.url as string, 'http://localhost'); + const { pathname: path } = parsedUrl; const routes = Object.keys(this.routes); for (let i = 0; i < routes.length; i += 1) { const route = routes[i]; @@ -217,7 +218,8 @@ export default class SocketModeReceiver implements Receiver { const pathMatch = matchRegex(path); if (pathMatch && this.routes[route][method] !== undefined) { const params = pathMatch.params as ParamsDictionary; - const message: ParamsIncomingMessage = Object.assign(req, { params }); + const query = parseSearchParams(parsedUrl.searchParams); + const message: ParamsIncomingMessage = Object.assign(req, { params, query }); this.routes[route][method](message, res); return; } @@ -291,3 +293,12 @@ export default class SocketModeReceiver implements Receiver { }); } } + +function parseSearchParams(searchParams: URLSearchParams): QueryDictionary { + const query: QueryDictionary = {}; + for (const key of searchParams.keys()) { + const values = searchParams.getAll(key); + query[key] = values.length === 1 ? values[0] : values; + } + return query; +}