UNPKG

@loopback/http-caching-proxy

Version:

A caching HTTP proxy for integration tests. NOT SUITABLE FOR PRODUCTION USE!

136 lines 5.34 kB
"use strict"; // Copyright IBM Corp. 2018,2019. All Rights Reserved. // Node module: @loopback/http-caching-proxy // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT Object.defineProperty(exports, "__esModule", { value: true }); const debugFactory = require("debug"); const http_1 = require("http"); const p_event_1 = require("p-event"); const makeRequest = require("request-promise-native"); const cacache = require('cacache'); const debug = debugFactory('loopback:caching-proxy'); const DEFAULT_OPTIONS = { port: 0, ttl: 24 * 60 * 60 * 1000, }; /** * The HTTP proxy implementation. */ class HttpCachingProxy { constructor(options) { this._options = Object.assign({}, DEFAULT_OPTIONS, options); if (!this._options.cachePath) { throw new Error('Required option missing: "cachePath"'); } this.url = 'http://proxy-not-running'; this._server = undefined; } /** * Start listening. */ async start() { this._server = http_1.createServer((request, response) => { this._handle(request, response); }); this._server.on('connect', (req, socket) => { socket.write('HTTP/1.1 501 Not Implemented\r\n\r\n'); socket.destroy(); }); this._server.listen(this._options.port); await p_event_1.default(this._server, 'listening'); const address = this._server.address(); this.url = `http://127.0.0.1:${address.port}`; } /** * Stop listening. */ async stop() { if (!this._server) return; this.url = 'http://proxy-not-running'; const server = this._server; this._server = undefined; server.close(); await p_event_1.default(server, 'close'); } _handle(request, response) { const onerror = (error) => { this.logError(request, error); response.statusCode = error.name === 'RequestError' ? 502 : 500; response.end(); }; try { this._handleAsync(request, response).catch(onerror); } catch (err) { onerror(err); } } async _handleAsync(request, response) { debug('Incoming request %s %s', request.method, request.url, request.headers); const cacheKey = this._getCacheKey(request); try { const entry = await cacache.get(this._options.cachePath, cacheKey); if (entry.metadata.createdAt + this._options.ttl > Date.now()) { debug('Sending cached response for %s', cacheKey); this._sendCachedEntry(entry.data, entry.metadata, response); return; } debug('Cache entry expired for %s', cacheKey); // (continue to forward the request) } catch (error) { if (error.code !== 'ENOENT') { console.warn('Cannot load cached entry.', error); } debug('Cache miss for %s', cacheKey); // (continue to forward the request) } await this._forwardRequest(request, response); } _getCacheKey(request) { // TODO(bajtos) consider adding selected/all headers to the key return `${request.method} ${request.url}`; } _sendCachedEntry(data, metadata, response) { response.writeHead(metadata.statusCode, metadata.headers); response.end(data); } async _forwardRequest(clientRequest, clientResponse) { const backendResponse = await makeRequest({ resolveWithFullResponse: true, simple: false, method: clientRequest.method, uri: clientRequest.url, headers: clientRequest.headers, body: clientRequest, }); debug('Got response for %s %s -> %s', clientRequest.method, clientRequest.url, backendResponse.statusCode, backendResponse.headers); const metadata = { statusCode: backendResponse.statusCode, headers: backendResponse.headers, createdAt: Date.now(), }; // Ideally, we should pipe the backend response to both // client response and cachache.put.stream. // r.pipe(clientResponse); // r.pipe(cacache.put.stream(...)) // To do so, we would have to defer .end() call on the client // response until the content is stored in the cache, // which is rather complex and involved. // Without that synchronization, the client can start sending // follow-up requests that won't be served from the cache as // the cache has not been updated yet. // Since this proxy is for testing only, buffering the entire // response body is acceptable. await cacache.put(this._options.cachePath, this._getCacheKey(clientRequest), backendResponse.body, { metadata }); clientResponse.writeHead(backendResponse.statusCode, backendResponse.headers); clientResponse.end(backendResponse.body); } logError(request, error) { console.log('Cannot proxy %s %s.', request.method, request.url, error.stack || error); } } exports.HttpCachingProxy = HttpCachingProxy; //# sourceMappingURL=http-caching-proxy.js.map