Skip to content

Commit ad905a5

Browse files
committed
fix(rest): parse query string even when there is no rest query param
See #2088
1 parent 82685e0 commit ad905a5

File tree

6 files changed

+75
-102
lines changed

6 files changed

+75
-102
lines changed

packages/rest/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"@types/express": "^4.11.1",
3131
"@types/express-serve-static-core": "^4.16.0",
3232
"@types/http-errors": "^1.6.1",
33-
"@types/parseurl": "^1.3.1",
3433
"@types/serve-static": "1.13.2",
3534
"@types/type-is": "^1.6.2",
3635
"ajv": "^6.5.1",
@@ -43,7 +42,6 @@
4342
"lodash": "^4.17.5",
4443
"openapi-schema-to-json-schema": "^2.1.0",
4544
"openapi3-ts": "^1.0.0",
46-
"parseurl": "^1.3.2",
4745
"path-to-regexp": "^2.2.0",
4846
"qs": "^6.5.2",
4947
"strong-error-handler": "^3.2.0",

packages/rest/src/parser.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,14 @@ import {
1010
ParameterObject,
1111
SchemasObject,
1212
} from '@loopback/openapi-v3-types';
13-
import * as debugModule from 'debug';
14-
import * as parseUrl from 'parseurl';
15-
import {parse as parseQuery} from 'qs';
13+
import * as debugFactory from 'debug';
1614
import {RequestBody, RequestBodyParser} from './body-parsers';
1715
import {coerceParameter} from './coercion/coerce-parameter';
1816
import {RestHttpErrors} from './rest-http-error';
1917
import {ResolvedRoute} from './router';
2018
import {OperationArgs, PathParameterValues, Request} from './types';
2119
import {validateRequestBody} from './validation/request-body.validator';
22-
23-
const debug = debugModule('loopback:rest:parser');
24-
25-
export const QUERY_NOT_PARSED = {};
26-
Object.freeze(QUERY_NOT_PARSED);
20+
const debug = debugFactory('loopback:rest:parser');
2721

2822
/**
2923
* Parses the request to derive arguments to be passed in for the Application
@@ -99,29 +93,15 @@ function getParamFromRequest(
9993
) {
10094
switch (spec.in) {
10195
case 'query':
102-
ensureRequestQueryWasParsed(request);
10396
return request.query[spec.name];
10497
case 'path':
10598
return pathParams[spec.name];
10699
case 'header':
107100
// @jannyhou TBD: check edge cases
108101
return request.headers[spec.name.toLowerCase()];
109-
break;
110102
// TODO(jannyhou) to support `cookie`,
111103
// see issue https://github.com/strongloop/loopback-next/issues/997
112104
default:
113105
throw RestHttpErrors.invalidParamLocation(spec.in);
114106
}
115107
}
116-
117-
function ensureRequestQueryWasParsed(request: Request) {
118-
if (request.query && request.query !== QUERY_NOT_PARSED) return;
119-
120-
const input = parseUrl(request)!.query;
121-
if (input && typeof input === 'string') {
122-
request.query = parseQuery(input);
123-
} else {
124-
request.query = {};
125-
}
126-
debug('Parsed request query: ', request.query);
127-
}

packages/rest/src/rest.server.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {ServeStaticOptions} from 'serve-static';
3131
import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers';
3232
import {HttpHandler} from './http-handler';
3333
import {RestBindings} from './keys';
34-
import {QUERY_NOT_PARSED} from './parser';
3534
import {RequestContext} from './request-context';
3635
import {
3736
ControllerClass,
@@ -193,15 +192,7 @@ export class RestServer extends Context implements Server, HttpServerLike {
193192

194193
protected _setupRequestHandler() {
195194
this._expressApp = express();
196-
197-
// Disable express' built-in query parser, we parse queries ourselves
198-
// Note that when disabled, express sets query to an empty object,
199-
// which makes it difficult for us to detect whether the query
200-
// has been parsed or not. At the same time, we want `request.query`
201-
// to remain as an object, because everybody in express ecosystem expects
202-
// that property to be defined. A static singleton object to the rescue!
203-
this._expressApp.set('query parser fn', (str: string) => QUERY_NOT_PARSED);
204-
195+
this._expressApp.set('query parser', 'extended');
205196
this.requestHandler = this._expressApp;
206197

207198
// Allow CORS support for all endpoints so that users

packages/rest/test/integration/rest.server.integration.ts

Lines changed: 54 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,39 @@
55

66
import {Application} from '@loopback/core';
77
import {
8-
supertest,
9-
expect,
108
createClientForHandler,
11-
itSkippedOnTravis,
12-
httpsGetAsync,
9+
expect,
1310
givenHttpServerConfig,
11+
httpsGetAsync,
12+
itSkippedOnTravis,
13+
supertest,
1414
} from '@loopback/testlab';
15+
import * as fs from 'fs';
16+
import {IncomingMessage, ServerResponse} from 'http';
17+
import * as yaml from 'js-yaml';
18+
import * as path from 'path';
19+
import {is} from 'type-is';
20+
import * as util from 'util';
1521
import {
16-
RestBindings,
17-
RestServer,
18-
RestComponent,
22+
BodyParser,
1923
get,
24+
post,
2025
Request,
26+
requestBody,
27+
RequestContext,
28+
RestBindings,
29+
RestComponent,
30+
RestServer,
2131
RestServerConfig,
22-
BodyParser,
2332
} from '../..';
24-
import {IncomingMessage, ServerResponse} from 'http';
25-
import * as yaml from 'js-yaml';
26-
import * as path from 'path';
27-
import * as fs from 'fs';
28-
import * as util from 'util';
2933
const readFileAsync = util.promisify(fs.readFile);
3034

31-
import {is} from 'type-is';
32-
import {requestBody, post} from '../../src';
33-
3435
const FIXTURES = path.resolve(__dirname, '../../../fixtures');
3536
const ASSETS = path.resolve(FIXTURES, 'assets');
3637

3738
describe('RestServer (integration)', () => {
3839
it('exports url property', async () => {
39-
// Explicitly setting host to IPv4 address so test runs on Travis
40-
const server = await givenAServer({rest: {port: 0, host: '127.0.0.1'}});
40+
const server = await givenAServer();
4141
server.handler(dummyRequestHandler);
4242
expect(server.url).to.be.undefined();
4343
await server.start();
@@ -52,8 +52,26 @@ describe('RestServer (integration)', () => {
5252
expect(server.url).to.be.undefined();
5353
});
5454

55+
it('parses query without decorated rest query params', async () => {
56+
// This handler responds with the query object (which is expected to
57+
// be parsed by Express)
58+
function requestWithQueryHandler({request, response}: RequestContext) {
59+
response.json(request.query);
60+
response.end();
61+
}
62+
63+
// See https://github.com/strongloop/loopback-next/issues/2088
64+
const server = await givenAServer();
65+
server.handler(requestWithQueryHandler);
66+
await server.start();
67+
await supertest(server.url)
68+
.get('/?x=1&y[a]=2')
69+
.expect(200, {x: '1', y: {a: '2'}});
70+
await server.stop();
71+
});
72+
5573
it('updates rest.port binding when listening on ephemeral port', async () => {
56-
const server = await givenAServer({rest: {port: 0}});
74+
const server = await givenAServer();
5775
await server.start();
5876
expect(server.getSync(RestBindings.PORT)).to.be.above(0);
5977
await server.stop();
@@ -73,7 +91,7 @@ describe('RestServer (integration)', () => {
7391
});
7492

7593
it('responds with 500 when Sequence fails with unhandled error', async () => {
76-
const server = await givenAServer({rest: {port: 0}});
94+
const server = await givenAServer();
7795
server.handler((context, sequence) => {
7896
return Promise.reject(new Error('unhandled test error'));
7997
});
@@ -111,11 +129,7 @@ describe('RestServer (integration)', () => {
111129

112130
it('allows static assets via api', async () => {
113131
const root = ASSETS;
114-
const server = await givenAServer({
115-
rest: {
116-
port: 0,
117-
},
118-
});
132+
const server = await givenAServer();
119133

120134
server.static('/html', root);
121135
const content = fs
@@ -129,11 +143,7 @@ describe('RestServer (integration)', () => {
129143

130144
it('allows static assets to be mounted on multiple paths', async () => {
131145
const root = ASSETS;
132-
const server = await givenAServer({
133-
rest: {
134-
port: 0,
135-
},
136-
});
146+
const server = await givenAServer();
137147

138148
server.static('/html-0', root);
139149
server.static('/html-1', root);
@@ -154,11 +164,7 @@ describe('RestServer (integration)', () => {
154164
it('merges different static asset directories when mounted on the same path', async () => {
155165
const root = ASSETS;
156166
const otherAssets = path.join(FIXTURES, 'other-assets');
157-
const server = await givenAServer({
158-
rest: {
159-
port: 0,
160-
},
161-
});
167+
const server = await givenAServer();
162168

163169
server.static('/html', root);
164170
server.static('/html', otherAssets);
@@ -178,11 +184,7 @@ describe('RestServer (integration)', () => {
178184

179185
it('allows static assets via api after start', async () => {
180186
const root = ASSETS;
181-
const server = await givenAServer({
182-
rest: {
183-
port: 0,
184-
},
185-
});
187+
const server = await givenAServer();
186188
await createClientForHandler(server.requestHandler)
187189
.get('/html/index.html')
188190
.expect(404);
@@ -196,11 +198,7 @@ describe('RestServer (integration)', () => {
196198

197199
it('allows non-static routes after assets', async () => {
198200
const root = ASSETS;
199-
const server = await givenAServer({
200-
rest: {
201-
port: 0,
202-
},
203-
});
201+
const server = await givenAServer();
204202
server.static('/html', root);
205203
server.handler(dummyRequestHandler);
206204

@@ -211,11 +209,7 @@ describe('RestServer (integration)', () => {
211209

212210
it('gives precedence to API routes over static assets', async () => {
213211
const root = ASSETS;
214-
const server = await givenAServer({
215-
rest: {
216-
port: 0,
217-
},
218-
});
212+
const server = await givenAServer();
219213
server.static('/html', root);
220214
server.handler(dummyRequestHandler);
221215

@@ -226,11 +220,7 @@ describe('RestServer (integration)', () => {
226220

227221
it('registers controllers defined later than static assets', async () => {
228222
const root = ASSETS;
229-
const server = await givenAServer({
230-
rest: {
231-
port: 0,
232-
},
233-
});
223+
const server = await givenAServer();
234224
server.static('/html', root);
235225
server.controller(DummyController);
236226

@@ -256,7 +246,7 @@ describe('RestServer (integration)', () => {
256246
}
257247
}
258248

259-
const server = await givenAServer({rest: {port: 0}});
249+
const server = await givenAServer();
260250
// Register a request body parser for xml
261251
server.bodyParser(XmlBodyParser);
262252
server.controller(DummyXmlController);
@@ -269,7 +259,7 @@ describe('RestServer (integration)', () => {
269259
});
270260

271261
it('allows cors', async () => {
272-
const server = await givenAServer({rest: {port: 0}});
262+
const server = await givenAServer();
273263
server.handler(dummyRequestHandler);
274264

275265
await createClientForHandler(server.requestHandler)
@@ -280,7 +270,7 @@ describe('RestServer (integration)', () => {
280270
});
281271

282272
it('allows cors preflight', async () => {
283-
const server = await givenAServer({rest: {port: 0}});
273+
const server = await givenAServer();
284274
server.handler(dummyRequestHandler);
285275

286276
await createClientForHandler(server.requestHandler)
@@ -311,11 +301,7 @@ describe('RestServer (integration)', () => {
311301
});
312302

313303
it('exposes "GET /openapi.json" endpoint', async () => {
314-
const server = await givenAServer({
315-
rest: {
316-
port: 0,
317-
},
318-
});
304+
const server = await givenAServer();
319305
const greetSpec = {
320306
responses: {
321307
200: {
@@ -407,7 +393,7 @@ describe('RestServer (integration)', () => {
407393
});
408394

409395
it('exposes "GET /openapi.yaml" endpoint', async () => {
410-
const server = await givenAServer({rest: {port: 0}});
396+
const server = await givenAServer();
411397
const greetSpec = {
412398
responses: {
413399
200: {
@@ -701,7 +687,10 @@ paths:
701687
await server.stop();
702688
});
703689

704-
async function givenAServer(options?: {rest: RestServerConfig}) {
690+
async function givenAServer(
691+
options: {rest: RestServerConfig} = {rest: {port: 0}},
692+
) {
693+
options.rest = givenHttpServerConfig(options.rest);
705694
const app = new Application(options);
706695
app.component(RestComponent);
707696
return await app.getServer(RestServer);

packages/rest/test/unit/coercion/utils.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ export async function testCoercion<T>(config: TestArgs<T>) {
5454
/* istanbul ignore next */
5555
try {
5656
const pathParams: PathParameterValues = {};
57-
58-
const req = givenRequest();
57+
let url = '/';
5958
const spec = givenOperationWithParameters([config.paramSpec]);
6059
const route = givenResolvedRoute(spec, pathParams);
6160

@@ -68,7 +67,7 @@ export async function testCoercion<T>(config: TestArgs<T>) {
6867
{aparameter: config.rawValue},
6968
{encodeValuesOnly: true},
7069
);
71-
req.url += `?${q}`;
70+
url += `?${q}`;
7271
break;
7372
case 'header':
7473
case 'cookie':
@@ -81,6 +80,10 @@ export async function testCoercion<T>(config: TestArgs<T>) {
8180
break;
8281
}
8382

83+
// Create the request after url is fully populated so that request.query
84+
// is parsed
85+
const req = givenRequest({url});
86+
8487
const requestBodyParser = new RequestBodyParser();
8588
if (config.expectError) {
8689
await expect(

packages/testlab/src/shot.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export function stubExpressContext(
107107
}
108108
request.app = app;
109109
request.originalUrl = request.url;
110+
parseQuery(request);
110111

111112
let response: express.Response | undefined;
112113
let result = new Promise<ObservedResponse>(resolve => {
@@ -131,6 +132,17 @@ export function stubExpressContext(
131132
return context;
132133
}
133134

135+
/**
136+
* Use `express.query` to parse the query string into `request.query` object
137+
* @param request Express http request object
138+
*/
139+
function parseQuery(request: express.Request) {
140+
// Use `express.query` to parse the query string
141+
// See https://github.com/expressjs/express/blob/master/lib/express.js#L79
142+
// See https://github.com/expressjs/express/blob/master/lib/middleware/query.js
143+
(express as any).query()(request, {}, () => {});
144+
}
145+
134146
function defineCustomContextInspect(
135147
context: HandlerContextStub,
136148
requestOptions: ShotRequestOptions,

0 commit comments

Comments
 (0)