Skip to content

Commit ac9065f

Browse files
authored
chore: api http router improvements (#38227)
1 parent 3ff4730 commit ac9065f

File tree

5 files changed

+64
-74
lines changed

5 files changed

+64
-74
lines changed

apps/meteor/app/api/server/middlewares/logger.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,24 @@ export const loggerMiddleware =
88
async (c, next) => {
99
const startTime = Date.now();
1010

11-
let payload = {};
12-
13-
// We don't want to consume the request body stream for multipart requests
14-
if (!c.req.header('content-type')?.includes('multipart/form-data')) {
15-
try {
16-
payload = await c.req.raw.clone().json();
17-
// eslint-disable-next-line no-empty
18-
} catch {}
19-
} else {
20-
payload = '[multipart/form-data]';
21-
}
22-
23-
const log = logger.logger.child({
24-
method: c.req.method,
25-
url: c.req.url,
26-
userId: c.req.header('x-user-id'),
27-
userAgent: c.req.header('user-agent'),
28-
length: c.req.header('content-length'),
29-
host: c.req.header('host'),
30-
referer: c.req.header('referer'),
31-
remoteIP: c.get('remoteAddress'),
32-
...(['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method) && getRestPayload(payload)),
33-
});
11+
const log = logger.logger.child(
12+
{
13+
method: c.req.method,
14+
url: c.req.url,
15+
userId: c.req.header('x-user-id'),
16+
userAgent: c.req.header('user-agent'),
17+
length: c.req.header('content-length'),
18+
host: c.req.header('host'),
19+
referer: c.req.header('referer'),
20+
remoteIP: c.get('remoteAddress'),
21+
...(['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method) && (await getRestPayload(c.req))),
22+
},
23+
{
24+
redact: [
25+
'payload.password', // Potentially logged by v1/login
26+
],
27+
},
28+
);
3429

3530
await next();
3631

apps/meteor/app/api/server/router.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ type HonoContext = Context<{
1010
Bindings: { incoming: IncomingMessage };
1111
Variables: {
1212
'remoteAddress': string;
13-
'bodyParams-override'?: Record<string, any>;
13+
'bodyParams': Record<string, unknown>;
14+
'bodyParams-override': Record<string, unknown> | undefined;
15+
'queryParams': Record<string, unknown>;
1416
};
1517
}>;
1618

1719
export type APIActionContext = {
1820
requestIp: string;
1921
urlParams: Record<string, string>;
20-
queryParams: Record<string, any>;
21-
bodyParams: Record<string, any>;
22+
queryParams: Record<string, unknown>;
23+
bodyParams: Record<string, unknown>;
2224
request: Request;
2325
path: string;
2426
response: any;
@@ -37,16 +39,14 @@ export class RocketChatAPIRouter<
3739
protected override convertActionToHandler(action: APIActionHandler): (c: HonoContext) => Promise<ResponseSchema<TypedOptions>> {
3840
return async (c: HonoContext): Promise<ResponseSchema<TypedOptions>> => {
3941
const { req, res } = c;
40-
const queryParams = this.parseQueryParams(req);
41-
const bodyParams = c.get('bodyParams-override') ?? (await this.parseBodyParams({ request: req }));
4242

4343
const request = req.raw.clone();
4444

4545
const context: APIActionContext = {
4646
requestIp: c.get('remoteAddress'),
4747
urlParams: req.param(),
48-
queryParams,
49-
bodyParams,
48+
queryParams: c.get('queryParams'),
49+
bodyParams: c.get('bodyParams-override') || c.get('bodyParams'),
5050
request,
5151
path: req.path,
5252
response: res,

apps/meteor/app/livechat/server/api/v1/customField.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,6 @@ const livechatCustomFieldsEndpoints = API.v1
111111
async function action() {
112112
const { customFieldId, customFieldData } = this.bodyParams;
113113

114-
if (!/^[0-9a-zA-Z-_]+$/.test(customFieldId)) {
115-
return API.v1.failure('Invalid custom field name. Use only letters, numbers, hyphens and underscores.');
116-
}
117-
118114
if (customFieldId) {
119115
const customField = await LivechatCustomField.findOneById(customFieldId);
120116
if (!customField) {

apps/meteor/server/lib/logger/logPayloads.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { HonoRequest } from 'hono';
2+
13
import { omit } from '../../../lib/utils/omit';
24

35
const { LOG_METHOD_PAYLOAD = 'false', LOG_REST_PAYLOAD = 'false', LOG_REST_METHOD_PAYLOADS = 'false' } = process.env;
@@ -23,7 +25,17 @@ export const getMethodArgs =
2325

2426
export const getRestPayload =
2527
LOG_REST_PAYLOAD === 'false' && LOG_REST_METHOD_PAYLOADS === 'false'
26-
? (): null => null
27-
: (payload: unknown): { payload: unknown } => ({
28-
payload,
29-
});
28+
? (): Promise<null> => Promise.resolve(null)
29+
: async (request: HonoRequest): Promise<{ payload: unknown } | null> => {
30+
if (request.header('content-type')?.includes('multipart/form-data')) {
31+
return { payload: '[multipart/form-data]' };
32+
}
33+
34+
try {
35+
return {
36+
payload: await request.raw.clone().json(),
37+
};
38+
} catch {
39+
return { payload: null };
40+
}
41+
};

packages/http-router/src/Router.ts

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,22 @@ export abstract class AbstractRouter<TActionCallback = (c: Context) => Promise<R
7878
protected abstract convertActionToHandler(action: TActionCallback): (c: Context) => Promise<ResponseSchema<TypedOptions>>;
7979
}
8080

81+
type InnerRouter = Hono<{
82+
Variables: {
83+
remoteAddress: string;
84+
bodyParams: Record<string, unknown>;
85+
queryParams: Record<string, unknown>;
86+
};
87+
}>;
88+
8189
export class Router<
8290
TBasePath extends string,
8391
TOperations extends {
8492
[x: string]: unknown;
8593
} = NonNullable<unknown>,
8694
TActionCallback = (c: Context) => Promise<ResponseSchema<TypedOptions>>,
8795
> extends AbstractRouter<TActionCallback> {
88-
protected innerRouter: Hono<{
89-
Variables: {
90-
remoteAddress: string;
91-
};
92-
}>;
96+
protected innerRouter: InnerRouter;
9397

9498
constructor(readonly base: TBasePath) {
9599
super();
@@ -150,39 +154,24 @@ export class Router<
150154
};
151155
}
152156

153-
protected async parseBodyParams<T extends Record<string, any>>({ request }: { request: HonoRequest; extra?: T }) {
157+
protected async parseBodyParams({ request }: { request: HonoRequest }): Promise<NonNullable<unknown>> {
154158
try {
155-
let parsedBody = {};
156-
const contentType = request.header('content-type');
159+
const contentType = request.header('content-type') || '';
157160

158-
if (contentType?.includes('multipart/form-data')) {
159-
// Don't parse multipart here, routes handle it manually via UploadService.parse()
160-
// since multipart/form-data is only used for file uploads
161-
return parsedBody;
161+
if (contentType.includes('application/json')) {
162+
return await request.raw.clone().json();
162163
}
163164

164-
if (contentType?.includes('application/json')) {
165-
parsedBody = await request.raw.clone().json();
166-
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
165+
if (contentType.includes('application/x-www-form-urlencoded')) {
167166
const req = await request.raw.clone().formData();
168-
parsedBody = Object.fromEntries(req.entries());
169-
} else {
170-
parsedBody = await request.raw.clone().text();
171-
}
172-
// This is necessary to keep the compatibility with the previous version, otherwise the bodyParams will be an empty string when no content-type is sent
173-
if (parsedBody === '') {
174-
return {};
167+
return Object.fromEntries(req.entries());
175168
}
176169

177-
if (Array.isArray(parsedBody)) {
178-
return parsedBody;
179-
}
180-
181-
return { ...parsedBody };
182-
// eslint-disable-next-line no-empty
183-
} catch {}
184-
185-
return {};
170+
return {};
171+
} catch {
172+
// No problem if there is error, just means the endpoint is going to have to parse the body itself if necessary
173+
return {};
174+
}
186175
}
187176

188177
protected parseQueryParams(request: HonoRequest) {
@@ -204,6 +193,7 @@ export class Router<
204193
let queryParams: Record<string, any>;
205194
try {
206195
queryParams = this.parseQueryParams(req);
196+
c.set('queryParams', queryParams);
207197
} catch (e) {
208198
logger.warn({ msg: 'Error parsing query params for request', path: req.path, err: e });
209199

@@ -233,6 +223,7 @@ export class Router<
233223
}
234224

235225
const bodyParams = await this.parseBodyParams({ request: req });
226+
c.set('bodyParams', bodyParams);
236227

237228
if (options.body) {
238229
const validatorFn = options.body;
@@ -439,11 +430,7 @@ export class Router<
439430
return router;
440431
}
441432

442-
getHonoRouter(): Hono<{
443-
Variables: {
444-
remoteAddress: string;
445-
};
446-
}> {
433+
getHonoRouter(): InnerRouter {
447434
return this.innerRouter;
448435
}
449436
}

0 commit comments

Comments
 (0)