Skip to content

Commit 6433718

Browse files
Aaron Hedgeserunion
Aaron Hedges
andauthored
fix: remove null postData, add support for nested express routes (#337)
* fix(node): no requestbody removes key, support nested express objects * fix(node): fix some comments Co-authored-by: Jon Ursenbach <[email protected]> * fix(node): fix test name Co-authored-by: Jon Ursenbach <[email protected]> * style: commenting out the default-param-last rule in tests * fix: shallow copying the requst destroys req.headers on node 16 Co-authored-by: Jon Ursenbach <[email protected]> Co-authored-by: Jon Ursenbach <[email protected]>
1 parent d7ea298 commit 6433718

File tree

5 files changed

+66
-20
lines changed

5 files changed

+66
-20
lines changed

packages/node/__tests__/.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "@readme/eslint-config/testing",
33
"rules": {
4+
"default-param-last": "off",
45
"jest/expect-expect": [
56
"error",
67
{

packages/node/__tests__/index.test.ts

+40-12
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,6 @@ describe('#metrics', () => {
108108
rimraf.sync(cacheDir);
109109
});
110110

111-
/* it('should error if missing apiKey', () => {
112-
expect(() => {
113-
expressMiddleware();
114-
}).toThrow('You must provide your ReadMe API key');
115-
});
116-
117-
it('should error if missing grouping function', () => {
118-
expect(() => {
119-
expressMiddleware('api-key');
120-
}).toThrow('You must provide a grouping function');
121-
}); */
122-
123111
it('should send a request to the metrics server', () => {
124112
const apiMock = getReadMeApiMock(1);
125113
const mock = nock(config.host, {
@@ -150,6 +138,45 @@ describe('#metrics', () => {
150138
});
151139
});
152140

141+
it('express should log the full request url with nested express apps', () => {
142+
const apiMock = getReadMeApiMock(1);
143+
const mock = nock(config.host, {
144+
reqheaders: {
145+
'Content-Type': 'application/json',
146+
'User-Agent': `${pkg.name}/${pkg.version}`,
147+
},
148+
})
149+
.post('/v1/request', ([body]) => {
150+
expect(body.group).toStrictEqual(outgoingGroup);
151+
expect(body.request.log.entries[0].request.url).toContain('/test/nested');
152+
return true;
153+
})
154+
.basicAuth({ user: apiKey })
155+
.reply(200);
156+
157+
const app = express();
158+
const appNest = express();
159+
160+
appNest.use(expressMiddleware(apiKey, () => incomingGroup));
161+
appNest.get('/nested', (req, res) => {
162+
// We're asserting `req.url` to be `/nested` here because the way that Express does contextual route loading
163+
// `req.url` won't include the `/test`. The `/test` is only added later internally in Express with `req.originalUrl`.
164+
expect(req.url).toBe('/nested');
165+
res.sendStatus(200);
166+
});
167+
168+
app.use('/test', appNest);
169+
170+
return request(app)
171+
.get('/test/nested')
172+
.expect(200)
173+
.expect(res => expect(res).toHaveDocumentationHeader())
174+
.then(() => {
175+
apiMock.done();
176+
mock.done();
177+
});
178+
});
179+
153180
it('should have access to group(req,res) objects', () => {
154181
const apiMock = getReadMeApiMock(1);
155182
const mock = nock(config.host, {
@@ -168,6 +195,7 @@ describe('#metrics', () => {
168195
.reply(200);
169196

170197
const app = express();
198+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
171199
app.use((req: any, res: any, next) => {
172200
req.a = 'a';
173201
res.b = 'b';

packages/node/__tests__/lib/process-request.test.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as http from 'http';
44
import processRequest from '../../src/lib/process-request';
55
import FormData from 'form-data';
66

7-
// eslint-disable-next-line default-param-last
87
function createApp(reqOptions?: LogOptions, shouldPreParse = false, bodyOverride?) {
98
const requestListener = function (req: http.IncomingMessage, res: http.ServerResponse) {
109
let body = '';
@@ -470,15 +469,15 @@ describe('#postData', () => {
470469
});
471470
});
472471

473-
test('should be an empty object if request is a GET', () =>
472+
test('should be undefined if request has no postData', () =>
474473
request(createApp())
475474
.get('/')
476-
.expect(({ body }) => expect(body.postData).toBeNull()));
475+
.expect(({ body }) => expect(body.postData).toBeUndefined()));
477476

478-
test('should be null if req.body is empty', () =>
477+
test('should be missing if req.body is empty', () =>
479478
request(createApp())
480479
.post('/')
481-
.expect(({ body }) => expect(body.postData).toBeNull()));
480+
.expect(({ body }) => expect(body.postData).toBeUndefined()));
482481

483482
test('#text should contain stringified body', () => {
484483
const body = { a: 1, b: 2 };

packages/node/src/lib/express-middleware.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,17 @@ export function expressMiddleware(readmeApiKey: string, group: GroupingFunction,
9797
const groupData = group(req, res);
9898

9999
const payload = constructPayload(
100-
req,
100+
{
101+
...req,
102+
103+
// Shallow copying `req` destroys `req.headers` on Node 16 so we're re-adding it.
104+
headers: req.headers,
105+
106+
// If you're using route nesting with `express.use()` then `req.url` is contextual to that route. So say
107+
// you have an `/api` route that loads `/v1/upload`, `req.url` within the `/v1/upload` controller will be
108+
// `/v1/upload`. Calling `req.originalUrl` ensures that we also capture the `/api` prefix.
109+
url: req.originalUrl,
110+
},
101111
res,
102112
{
103113
...groupData,

packages/node/src/lib/process-request.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export function fixHeader(header: string | number | Array<string>): string | und
3737
* @returns A redacted string potentially containing the length of the original value, if it was a string
3838
*/
3939
function redactValue(value: string) {
40-
return `[REDACTED${typeof value === 'string' ? ` ${value.length}` : ''}]`;
40+
const redactedVal = typeof value === 'string' ? ` ${value.length}` : '';
41+
return `[REDACTED${redactedVal}]`;
4142
}
4243

4344
/**
@@ -179,7 +180,7 @@ export default function processRequest(
179180
// We only ever use this reqUrl with the fake hostname for the pathname and querystring.
180181
const reqUrl = new URL(req.url, `${protocol}://readme.io`);
181182

182-
return {
183+
const requestData = {
183184
method: req.method,
184185
url: url.format({
185186
protocol,
@@ -197,4 +198,11 @@ export default function processRequest(
197198
headersSize: 0,
198199
bodySize: 0,
199200
};
201+
202+
// At the moment the server doesn't accept null for request bodies. We're opening up support soon, but getting this fix out will be faster.
203+
if (requestData.postData === null) {
204+
delete requestData.postData;
205+
}
206+
207+
return requestData;
200208
}

0 commit comments

Comments
 (0)