UNPKG

11.3 kBJavaScriptView Raw
1/**
2 * Kettle Requests
3 *
4 * Copyright 2012-2013 OCAD University
5 *
6 * Licensed under the New BSD license. You may not use this file except in
7 * compliance with this License.
8 *
9 * You may obtain a copy of the License at
10 * https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
11 */
12
13"use strict";
14
15var fluid = fluid || require("infusion"),
16 kettle = fluid.registerNamespace("kettle");
17
18fluid.defaults("kettle.requests", {
19 gradeNames: ["fluid.component"],
20 events: {
21 createRequest: null // fired by our handler once express determines that route + verb is a match
22 },
23 dynamicComponents: {
24 request: {
25 createOnEvent: "createRequest",
26 type: "{arguments}.0.type",
27 options: "{arguments}.0.options"
28 }
29 }
30});
31
32// The member name of the currently active request as held in Infusion's "resolve root" component
33kettle.requestMemberName = fluid.typeNameToMemberName("kettle.request");
34
35kettle.getCurrentRequest = function () {
36 return fluid.resolveRootComponent[kettle.requestMemberName];
37};
38
39kettle.markActiveRequest = function (request) {
40 var parent = fluid.resolveRootComponent,
41 memberName = kettle.requestMemberName,
42 instantiator = fluid.globalInstantiator,
43 innerRequest = kettle.getCurrentRequest();
44 if (request && innerRequest && innerRequest !== request) {
45 fluid.fail("Error marking thread to request " + request.id + " - this thread is already marked to request " + innerRequest.id + " . Make sure to invoke this request asynchronously.");
46 }
47 if (request) {
48 if (innerRequest !== request) {
49 if (fluid.isDestroyed(request)) {
50 fluid.fail("Error marking thread to request " + request.id + " which has already been destroyed");
51 }
52 instantiator.recordKnownComponent(parent, request, memberName, false);
53 }
54 } else {
55 if (parent[memberName]) { // unmarked automatically if destroyed
56 instantiator.clearComponent(parent, memberName);
57 }
58 }
59 return innerRequest;
60};
61
62// Returns a function wrapping the supplied callback with the supplied request - the
63// callback will be executed in an environment where the request has been restored
64kettle.withRequest = function (request, callback) {
65 return function wrappedCallback() {
66 // Avoid double-wrapping the same stack, just as a courtesy for debugging and efficiency
67 // To be compatible to both when.js < 2.0.0 and when >= 2.0.0 we must be prepared for superfluous wrapping sometimes
68 // Although in theory this could be removed now that when.js is gone, there are still use cases where the user may supply, e.g.
69 // synchronously resolving promises.
70 if (request && fluid.isDestroyed(request)) {
71 fluid.log("Failing to resume callback for request " + request.id + " which has already concluded");
72 return;
73 }
74 var innerRequest = kettle.markActiveRequest(request);
75 if (innerRequest) {
76 return callback.apply(null, arguments);
77 } else {
78 try {
79 return callback.apply(null, arguments);
80 } catch (err) { // ensure we handle this before we lose marking
81 fluid.onUncaughtException.fire(err);
82 } finally {
83 kettle.markActiveRequest(null);
84 }
85 }
86 };
87};
88
89// Every genuinely asynchronous callback propagated by a Kettle app must be wrapped by this
90// function - in order to propagate the "marker" which identifies the current request object.
91kettle.wrapCallback = function (callback) {
92 var request = kettle.getCurrentRequest();
93 return kettle.withRequest(request, callback);
94};
95
96fluid.defaults("kettle.request", {
97 gradeNames: ["fluid.component"],
98 mergePolicy: {
99 requestMiddleware: "noexpand"
100 },
101 invokers: {
102 handleRequest: "fluid.notImplemented",
103 handleFullRequest: "fluid.notImplemented"
104 },
105 events: {
106 onHandle: null, // Fired in order to execute its listener, handleRequest, which invokes the user's handler
107 onSuccess: null, // A convenient proxy for the main request handler to report disposition (equivalent to handlerPromise)
108 onError: null, // A convenient proxy for the main request handler to report disposition (equivalent to handlerPromise)
109 onRequestEnd: null,
110 onRequestSuccess: null, // Overall disposition of the entire request - handler actually sends response
111 onRequestError: null // Overall disposition of the entire request - handler actually sends response
112 },
113 // sourced from dynamic args
114 req: "{arguments}.1",
115 res: "{arguments}.2",
116 next: "{arguments}.3",
117 members: {
118 req: "{that}.options.req", // TODO: can't supply these direct from args because of framework members merging bug - get array instead
119 res: "{that}.options.res",
120 next: "{that}.options.next",
121 /* Construct a "promisified" proxy for the handling of this request. It will
122 * have handlers registered which forward to this request's success and error events.
123 * The promise may be rejected or resolved by the user to represent that disposition
124 * for the overall request */
125 handlerPromise: "@expand:fluid.promise()"
126 },
127 listeners: {
128 "onCreate.activate": "kettle.request.activate",
129 "onHandle.handleRequest": {
130 funcName: "kettle.request.handleRequest",
131 args: "{that}"
132 },
133 "onSuccess.forward": "kettle.request.forwardPromise(resolve, {that}.handlerPromise, {arguments}.0)",
134 "onError.forward": "kettle.request.forwardPromise(reject, {that}.handlerPromise, {arguments}.0)"
135 }
136});
137
138// A handler which reports a 404 if the request has not already been handled (e.g. by some middleware, e.g. static)
139kettle.request.notFoundHandler = function (request) {
140 /* istanbul ignore else - much middleware is naughty and does not call "next" if it thinks it has fully handled the request */
141 if (!request.handlerPromise.disposition) {
142 request.handlerPromise.reject({statusCode: 404, message: "Cannot " + request.req.method + " " + request.req.originalUrl});
143 }
144};
145
146kettle.request.forwardPromise = function (method, promise, val) {
147 if (!promise.disposition) {
148 promise[method](val);
149 } else {
150 // Don't use fluid.fail here to avoid infinite triggering of errors
151 fluid.log("Error in forwarding result ", val, " to promise " + method + ": promise has already received " + promise.disposition);
152 }
153};
154
155kettle.request.activate = function (that) {
156 that.req.fluidRequest = that;
157 kettle.markActiveRequest(that);
158};
159
160kettle.request.clear = function () {
161 kettle.markActiveRequest(null);
162};
163
164kettle.request.handleRequest = function (that) {
165 try {
166 that.handleRequest(that);
167 } catch (err) {
168 if (!that.handlerPromise.disposition) {
169 that.handlerPromise.reject({message: err.message, stack: err.stack});
170 }
171 } finally {
172 kettle.markActiveRequest(null);
173 }
174};
175
176// A function representing the "handler executing task" of the request's sequence
177
178kettle.request.handleRequestTask = function (request) {
179 if (!request.res.finished) { // don't handle if some middleware has already sent a full response - makes us deterministic on node 4
180 request.events.onHandle.fire(request); // our actual request handler triggerer
181 }
182 return request.handlerPromise;
183};
184
185fluid.defaults("kettle.request.mismatch", {
186 requestMiddleware: {
187 mismatch: {
188 middleware: "{middlewareHolder}.mismatch",
189 priority: "first"
190 }
191 }
192});
193
194fluid.defaults("kettle.request.http", {
195 gradeNames: ["kettle.request"],
196 invokers: {
197 handleFullRequest: "kettle.request.http.handleFullRequest"
198 },
199 listeners: {
200 "onCreate.ensureResponseDisposes": {
201 funcName: "kettle.request.http.ensureResponseDisposes",
202 priority: "before:handleRequest"
203 },
204 "onRequestError.handle": {
205 funcName: "kettle.request.http.errorHandler",
206 args: ["{that}.res", "{arguments}.0"]
207 },
208 "onRequestSuccess.handle": {
209 funcName: "kettle.request.http.successHandler",
210 args: ["{that}", "{arguments}.0"]
211 }
212 }
213});
214
215
216fluid.defaults("kettle.request.http.mismatch", {
217 gradeNames: ["kettle.request.http", "kettle.request.mismatch"],
218 invokers: {
219 handleRequest: "fluid.identity"
220 }
221});
222
223kettle.request.http.handleFullRequest = function (request, fullRequestPromise, next) {
224 fullRequestPromise.then(function (response) {
225 request.events.onRequestSuccess.fire(response);
226 next();
227 }, function (err) {
228 request.events.onRequestError.fire(err);
229 // A variant implementation could decide to call next(err) if we were interested in invoking express' upstream handler
230 next();
231 });
232};
233
234/**
235 * Send an error payload to the client if the request ends in error
236 * @param {Object} res an Express response object.
237 * @param {Object} error an error payload. Should include fields <code>statusCode</code> holding a numeric HTTP status code, and <code>message</code> holding an error message.
238 */
239kettle.request.http.errorHandler = function (res, error) {
240 var outError = fluid.extend(true, {
241 isError: true,
242 message: "Unknown error",
243 statusCode: 500
244 }, error);
245 if (error.message) { // Error object's "message" property fails to clone regularly on node 4.x
246 outError.message = error.message;
247 }
248 res.status(outError.statusCode).json(fluid.censorKeys(outError, ["statusCode"]));
249 return outError;
250};
251
252/**
253 * Send a successful payload to the client if the success event is fired.
254 * @param {Object} res an Express response object.
255 * @param {Object} response a success payload.
256 */
257kettle.request.http.successHandler = function (request, response) {
258 if (request.req.method.toLowerCase() === "options") {
259 request.res.status(200).end();
260 return;
261 }
262 if (typeof(response) !== "string") {
263 request.res.json(response);
264 } else {
265 request.res.status(200).send(response);
266 }
267};
268
269/**
270 * Ensure that the response is properly disposed of when finished.
271 * @param {Object} that request object.
272 */
273kettle.request.http.ensureResponseDisposes = function (that) {
274 // NOTE: This is here because any of these events can represent the
275 // moment when the server is finished with the response.
276 // TODO: We perhaps want to extend the possibility that further handlers may interpose
277 // AFTER the main "handler" - in which case this timing point just becomes a prerequisite for
278 // disposal rather than the direct trigger
279 fluid.each(["close", "finish", "end", "error"], function addListener(event) {
280 that.res.on(event, function eventListener() {
281 // TODO: need to write a test case to validate possibility that this request may be destroyed
282 if (!fluid.isDestroyed(that)) {
283 that.events.onRequestEnd.fire();
284 that.destroy();
285 }
286 });
287 });
288};