Skip to content

Commit a92ccce

Browse files
committed
Propagate the request ID when issuing requests; rel v0.7.0
This commit changes the API of the Request object and the utility functions used to contact the MW and REST APIs. The Request object now has a `context` property that includes the request ID. Additionally, the request now features an `issueRequest()` method which should be used instead of `preq`. It augments the request by setting the user-agent and x-request-id headers automatically, in this way propagating the request ID further down the call chain. In order to account for these changes, `mwApiGet()` and `restApiGet()` have been changed to take the request as a parameter instead of the application object. Bug: T225711
1 parent b3b59ba commit a92ccce

File tree

7 files changed

+111
-60
lines changed

7 files changed

+111
-60
lines changed

lib/api-util.js

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
'use strict';
22

3-
const preq = require('preq');
3+
const BBPromise = require('bluebird');
44
const sUtil = require('./util');
55
const Template = require('swagger-router').Template;
66
const HTTPError = sUtil.HTTPError;
77

88
/**
99
* Calls the MW API with the supplied query as its body
10-
* @param {!Object} app the application object
11-
* @param {string} domain the domain to issue the request to
10+
* @param {!Object} req the incoming request object
1211
* @param {?Object} query an object with all the query parameters for the MW API
12+
* @param {?Object} headers additional headers to pass to the MW API
1313
* @return {!Promise} a promise resolving as the response object from the MW API
1414
*/
15-
function mwApiGet(app, domain, query) {
15+
function mwApiGet(req, query, headers) {
1616

17+
const app = req.app;
1718
query = Object.assign({
1819
format: 'json',
1920
formatversion: 2
2021
}, query);
2122

2223
const request = app.mwapi_tpl.expand({
2324
request: {
24-
params: { domain },
25-
headers: { 'user-agent': app.conf.user_agent },
25+
params: { domain: req.params.domain },
26+
headers: req.headers,
2627
query
2728
}
2829
});
30+
Object.assign(request.headers, headers);
2931

30-
return preq(request).then((response) => {
32+
return req.issueRequest(request).then((response) => {
3133
if (response.status < 200 || response.status > 399) {
3234
// there was an error when calling the upstream service, propagate that
33-
throw new HTTPError({
35+
return BBPromise.reject(new HTTPError({
3436
status: response.status,
3537
type: 'api_error',
3638
title: 'MW API error',
3739
detail: response.body
38-
});
40+
}));
3941
}
4042
return response;
4143
});
@@ -44,9 +46,8 @@ function mwApiGet(app, domain, query) {
4446

4547
/**
4648
* Calls the REST API with the supplied domain, path and request parameters
47-
* @param {!Object} app the application object
48-
* @param {string} domain the domain to issue the request for
49-
* @param {!string} path the REST API path to contact without the leading slash
49+
* @param {!Object} req the incoming request object
50+
* @param {?string} path the REST API path to contact without the leading slash
5051
* @param {?Object} [restReq={}] the object containing the REST request details
5152
* @param {?string} [restReq.method=get] the request method
5253
* @param {?Object} [restReq.query={}] the query string to send, if any
@@ -55,22 +56,32 @@ function mwApiGet(app, domain, query) {
5556
* @return {!Promise} a promise resolving as the response object from the REST API
5657
*
5758
*/
58-
function restApiGet(app, domain, path, restReq) {
59+
function restApiGet(req, path, restReq) {
5960

61+
const app = req.app;
62+
if (path.constructor === Object) {
63+
restReq = path;
64+
path = undefined;
65+
}
6066
restReq = restReq || {};
61-
path = path[0] === '/' ? path.slice(1) : path;
62-
63-
const request = app.restbase_tpl.expand({
64-
request: {
65-
method: restReq.method,
66-
params: { domain, path },
67-
query: restReq.query,
68-
headers: Object.assign({ 'user-agent': app.conf.user_agent }, restReq.headers),
69-
body: restReq.body
70-
}
71-
});
67+
restReq.method = restReq.method || 'get';
68+
restReq.query = restReq.query || {};
69+
restReq.headers = restReq.headers || {};
70+
restReq.params = restReq.params || {};
71+
restReq.params.path = path || restReq.params.path;
72+
restReq.params.domain = restReq.params.domain || req.params.domain;
73+
if (!restReq.params.path || !restReq.params.domain) {
74+
return BBPromise.reject(new HTTPError({
75+
status: 500,
76+
type: 'internal_error',
77+
title: 'Invalid internal call',
78+
detail: 'domain and path need to be defined for the REST API call'
79+
}));
80+
}
81+
restReq.params.path = restReq.params.path[0] === '/' ?
82+
restReq.params.path.slice(1) : restReq.params.path;
7283

73-
return preq(request);
84+
return req.issueRequest(app.restbase_tpl.expand({ request: restReq }));
7485

7586
}
7687

@@ -83,10 +94,9 @@ function setupApiTemplates(app) {
8394
// set up the MW API request template
8495
if (!app.conf.mwapi_req) {
8596
app.conf.mwapi_req = {
97+
method: 'post',
8698
uri: 'http://{{domain}}/w/api.php',
87-
headers: {
88-
'user-agent': '{{user-agent}}'
89-
},
99+
headers: '{{request.headers}}',
90100
body: '{{ default(request.query, {}) }}'
91101
};
92102
}

lib/util.js

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const BBPromise = require('bluebird');
4+
const preq = require('preq');
45
const express = require('express');
56
const uuidv1 = require('uuid/v1');
67
const bunyan = require('bunyan');
@@ -86,14 +87,6 @@ function errForLog(err) {
8687

8788
}
8889

89-
/**
90-
* Generates a unique request ID.
91-
* @return {!string} the generated request ID
92-
*/
93-
function generateRequestId() {
94-
return uuidv1();
95-
}
96-
9790
/**
9891
* Wraps all of the given router's handler functions with
9992
* promised try blocks so as to allow catching all errors,
@@ -239,12 +232,38 @@ function createRouter(opts) {
239232
*/
240233
function initAndLogRequest(req, app) {
241234
req.headers = req.headers || {};
242-
req.headers['x-request-id'] = req.headers['x-request-id'] || generateRequestId();
235+
req.headers['x-request-id'] = req.headers['x-request-id'] || uuidv1();
243236
req.logger = app.logger.child({
244237
request_id: req.headers['x-request-id'],
245238
request: reqForLog(req, app.conf.log_header_whitelist)
246239
});
247-
req.logger.log('trace/req', { msg: 'incoming request' });
240+
req.context = { reqId: req.headers['x-request-id'] };
241+
req.issueRequest = (request) => {
242+
if (!(request.constructor === Object)) {
243+
request = { uri: request };
244+
}
245+
if (request.url) {
246+
request.uri = request.url;
247+
delete request.url;
248+
}
249+
if (!request.uri) {
250+
return BBPromise.reject(new HTTPError({
251+
status: 500,
252+
type: 'internal_error',
253+
title: 'No request to issue',
254+
detail: 'No request has been specified'
255+
}));
256+
}
257+
request.method = request.method || 'get';
258+
request.headers = request.headers || {};
259+
Object.assign(request.headers, {
260+
'user-agent': app.conf.user_agent,
261+
'x-request-id': req.context.reqId
262+
});
263+
req.logger.log('trace/req', { msg: 'Outgoing request', out_request: request });
264+
return preq(request);
265+
};
266+
req.logger.log('trace/req', { msg: 'Incoming request' });
248267
}
249268

250269
module.exports = {

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "service-template-node",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "A blueprint for MediaWiki REST API services",
55
"main": "./app.js",
66
"scripts": {
@@ -22,7 +22,7 @@
2222
"service template",
2323
"MediaWiki"
2424
],
25-
"author": "Wikimedia Service Team <services@wikimedia.org>",
25+
"author": "Wikimedia Service Team <services@lists.wikimedia.org>",
2626
"contributors": [],
2727
"license": "Apache-2.0",
2828
"bugs": {
@@ -33,14 +33,14 @@
3333
"bluebird": "^3.5.5",
3434
"body-parser": "^1.19.0",
3535
"bunyan": "^1.8.12",
36-
"compression": "^1.7.3",
36+
"compression": "^1.7.4",
3737
"domino": "^2.1.3",
3838
"express": "^4.17.1",
3939
"http-shutdown": "^1.2.1",
4040
"js-yaml": "^3.13.1",
41-
"preq": "^0.5.8",
41+
"preq": "^0.5.9",
4242
"service-runner": "^2.7.1",
43-
"swagger-router": "^0.7.3",
43+
"swagger-router": "^0.7.4",
4444
"swagger-ui-dist": "^3.22.3",
4545
"uuid": "^3.3.2"
4646
},

routes/empty.js.template

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33

44
var BBPromise = require('bluebird');
5-
var preq = require('preq');
65
var sUtil = require('../lib/util');
76

87
// shortcut

routes/ex.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ router.get('/err/manual/auth', (req, res) => {
103103

104104
});
105105

106+
/*
107+
* REQUEST EXAMPLES
108+
*/
109+
110+
/**
111+
* GET /req/uri
112+
*/
113+
router.get('/req/uri/:uri', (req, res) => {
114+
// to issue an external request, use req.issueRequest
115+
return req.issueRequest(req.params.uri)
116+
.then((r) => {
117+
res.status(r.status);
118+
res.set(r.headers);
119+
res.end(r.body);
120+
});
121+
});
122+
106123
module.exports = (appObj) => {
107124

108125
return {

routes/v1.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ const HTTPError = sUtil.HTTPError;
1313
*/
1414
const router = sUtil.router();
1515

16-
/**
17-
* The main application object reported when this module is require()d
18-
*/
19-
let app;
20-
2116
/**
2217
* GET /siteinfo{/prop}
2318
* Fetches site info for the given domain, optionally
@@ -44,7 +39,7 @@ router.get('/siteinfo/:prop?', (req, res) => {
4439
};
4540

4641
// send it
47-
return apiUtil.mwApiGet(app, req.params.domain, apiQuery)
42+
return apiUtil.mwApiGet(req, apiQuery)
4843
// and then return the result to the caller
4944
.then((apiRes) => {
5045
// do we have to return only one prop?
@@ -79,14 +74,13 @@ router.get('/siteinfo/:prop?', (req, res) => {
7974
/**
8075
* A helper function that obtains the Parsoid HTML for a given title and
8176
* loads it into a domino DOM document instance.
82-
* @param {string} domain the domain to contact
83-
* @param {string} title the title of the page to get
77+
* @param {!Object} req the incoming request
8478
* @return {Promise} a promise resolving as the HTML element object
8579
*/
86-
function getBody(domain, title) {
80+
function getBody(req) {
8781

8882
// get the page
89-
return apiUtil.restApiGet(app, domain, `page/html/${title}`)
83+
return apiUtil.restApiGet(req, `page/html/${req.params.title}`)
9084
.then((callRes) => {
9185
// and then load and parse the page
9286
return BBPromise.resolve(domino.createDocument(callRes.body));
@@ -101,7 +95,7 @@ function getBody(domain, title) {
10195
router.get('/page/:title', (req, res) => {
10296

10397
// get the page's HTML directly
104-
return getBody(req.params.domain, req.params.title)
98+
return getBody(req)
10599
// and then return it
106100
.then((doc) => {
107101
res.status(200).type('html').end(doc.body.innerHTML);
@@ -116,7 +110,7 @@ router.get('/page/:title', (req, res) => {
116110
router.get('/page/:title/lead', (req, res) => {
117111

118112
// get the page's HTML directly
119-
return getBody(req.params.domain, req.params.title)
113+
return getBody(req)
120114
// and then find the leading section and return it
121115
.then((doc) => {
122116
let leadSec = '';
@@ -139,8 +133,6 @@ router.get('/page/:title/lead', (req, res) => {
139133

140134
module.exports = (appObj) => {
141135

142-
app = appObj;
143-
144136
return {
145137
path: '/',
146138
api_version: 1,

spec.template.yaml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
openapi: 3.0.0
22
info:
3-
version: 0.6.0
3+
version: 0.7.0
44
title: WMF Node.JS Service Template
55
description: A template for creating Node.JS services
66
termsOfService: https://wikimediafoundation.org/wiki/Terms_of_Use
@@ -426,6 +426,20 @@ paths:
426426
status: 401
427427
headers:
428428
content-type: application/json
429+
/ex/req/uri/{uri}:
430+
get:
431+
tags:
432+
- Example
433+
- Request issuing
434+
description: Issues a request to the given URI
435+
responses:
436+
200:
437+
description: OK
438+
x-amples:
439+
- title: Get example home page
440+
request:
441+
params:
442+
uri: 'http://www.example.com'
429443

430444
components:
431445
schemas:
@@ -470,4 +484,4 @@ components:
470484
schema:
471485
type: string
472486
description: |
473-
Site info prop.
487+
Site info prop.

0 commit comments

Comments
 (0)