UNPKG

openhim-core

Version:

The OpenHIM core application that provides logging and routing of http requests

259 lines (222 loc) 8.4 kB
import logger from 'winston' import * as transactions from '../model/transactions' import * as autoRetryUtils from '../autoRetry' import * as utils from '../utils' import { config } from '../config' import * as metrics from '../metrics' import { promisify } from 'util' export const transactionStatus = { PROCESSING: 'Processing', SUCCESSFUL: 'Successful', COMPLETED: 'Completed', COMPLETED_W_ERR: 'Completed with error(s)', FAILED: 'Failed' } function copyMapWithEscapedReservedCharacters (map) { const escapedMap = {} for (let k in map) { const v = map[k] if ((k.indexOf('.') > -1) || (k.indexOf('$') > -1)) { k = k.replace('.', '\uff0e').replace('$', '\uff04') } escapedMap[k] = v } return escapedMap } export function storeTransaction (ctx, done) { logger.info('Storing request metadata for inbound transaction') ctx.requestTimestamp = new Date() const headers = copyMapWithEscapedReservedCharacters(ctx.header) const tx = new transactions.TransactionModel({ status: transactionStatus.PROCESSING, clientID: (ctx.authenticated != null ? ctx.authenticated._id : undefined), channelID: ctx.authorisedChannel._id, clientIP: ctx.ip, request: { host: (ctx.host != null ? ctx.host.split(':')[0] : undefined), port: (ctx.host != null ? ctx.host.split(':')[1] : undefined), path: ctx.path, headers, querystring: ctx.querystring, body: ctx.body, method: ctx.method, timestamp: ctx.requestTimestamp } }) if (ctx.parentID && ctx.taskID) { tx.parentID = ctx.parentID tx.taskID = ctx.taskID } if (ctx.currentAttempt) { tx.autoRetryAttempt = ctx.currentAttempt } // check if channel request body is false and remove - or if request body is empty if ((ctx.authorisedChannel.requestBody === false) || (tx.request.body === '')) { // reset request body tx.request.body = '' // check if method is POST|PUT|PATCH - rerun not possible without request body if ((ctx.method === 'POST') || (ctx.method === 'PUT') || (ctx.method === 'PATCH')) { tx.canRerun = false } } if (utils.enforceMaxBodiesSize(ctx, tx.request)) { tx.canRerun = false } return tx.save((err, tx) => { if (err) { logger.error(`Could not save transaction metadata: ${err}`) return done(err) } else { ctx.transactionId = tx._id ctx.header['X-OpenHIM-TransactionID'] = tx._id.toString() return done(null, tx) } }) } export function storeResponse (ctx, done) { const headers = copyMapWithEscapedReservedCharacters(ctx.response.header) const res = { status: ctx.response.status, headers, body: !ctx.response.body ? '' : ctx.response.body.toString(), timestamp: ctx.response.timestamp } // check if channel response body is false and remove if (ctx.authorisedChannel.responseBody === false) { // reset request body - primary route res.body = '' } const update = { response: res, error: ctx.error, orchestrations: [] } utils.enforceMaxBodiesSize(ctx, update.response) if (ctx.mediatorResponse) { if (ctx.mediatorResponse.orchestrations) { update.orchestrations.push(...truncateOrchestrationBodies(ctx, ctx.mediatorResponse.orchestrations)) } if (ctx.mediatorResponse.properties) { update.properties = ctx.mediatorResponse.properties } } if (ctx.orchestrations) { update.orchestrations.push(...truncateOrchestrationBodies(ctx, ctx.orchestrations)) } return transactions.TransactionModel.findOneAndUpdate({_id: ctx.transactionId}, update, {runValidators: true}, (err, tx) => { if (err) { logger.error(`Could not save response metadata for transaction: ${ctx.transactionId}. ${err}`) return done(err) } if ((tx === undefined) || (tx === null)) { logger.error(`Could not find transaction: ${ctx.transactionId}`) return done(err) } logger.info(`stored primary response for ${tx._id}`) return done() }) } function truncateOrchestrationBodies (ctx, orchestrations) { return orchestrations.map(orch => { const truncatedOrchestration = Object.assign({}, orch) if (truncatedOrchestration.request && truncatedOrchestration.request.body) { utils.enforceMaxBodiesSize(ctx, truncatedOrchestration.request) } if (truncatedOrchestration.response && truncatedOrchestration.response.body) { utils.enforceMaxBodiesSize(ctx, truncatedOrchestration.response) } return truncatedOrchestration }) } export function storeNonPrimaryResponse (ctx, route, done) { // check if channel response body is false and remove if (ctx.authorisedChannel.responseBody === false) { route.response.body = '' } if (ctx.transactionId != null) { if ((route.request != null ? route.request.body : undefined) != null) { utils.enforceMaxBodiesSize(ctx, route.request) } if ((route.response != null ? route.response.body : undefined) != null) { utils.enforceMaxBodiesSize(ctx, route.response) } transactions.TransactionModel.findByIdAndUpdate(ctx.transactionId, {$push: {routes: route}}, (err, tx) => { if (err) { logger.error(err) } return done(tx) }) } else { return logger.error('the request has no transactionId') } } /** * Set the status of the transaction based on the outcome of all routes. * * If the primary route responded in the mediator format and included a status * then that overrides all other status calculations. * * This should only be called once all routes have responded. */ export function setFinalStatus (ctx, callback) { let transactionId = '' if (ctx.request != null && ctx.request.header != null && ctx.request.header['X-OpenHIM-TransactionID'] != null) { transactionId = ctx.request.header['X-OpenHIM-TransactionID'] } else { transactionId = ctx.transactionId.toString() } return transactions.TransactionModel.findById(transactionId, (err, tx) => { if (err) { return callback(err) } const update = {} if ((ctx.mediatorResponse != null ? ctx.mediatorResponse.status : undefined) != null) { logger.debug(`The transaction status has been set to ${ctx.mediatorResponse.status} by the mediator`) update.status = ctx.mediatorResponse.status } else { let routeFailures = false let routeSuccess = true if (ctx.routes) { for (const route of Array.from(ctx.routes)) { if (route.response.status >= 500 && route.response.status <= 599) { routeFailures = true } if (!(route.response.status >= 200 && route.response.status <= 299)) { routeSuccess = false } } } if (ctx.response.status >= 500 && ctx.response.status <= 599) { tx.status = transactionStatus.FAILED } else { if (routeFailures) { tx.status = transactionStatus.COMPLETED_W_ERR } if ((ctx.response.status >= 200 && ctx.response.status <= 299) && routeSuccess) { tx.status = transactionStatus.SUCCESSFUL } if ((ctx.response.status >= 400 && ctx.response.status <= 499) && routeSuccess) { tx.status = transactionStatus.COMPLETED } } // In all other cases mark as completed if (tx.status === 'Processing') { tx.status = transactionStatus.COMPLETED } ctx.transactionStatus = tx.status logger.info(`Final status for transaction ${tx._id} : ${tx.status}`) update.status = tx.status } if (ctx.autoRetry != null) { if (!autoRetryUtils.reachedMaxAttempts(tx, ctx.authorisedChannel)) { update.autoRetry = ctx.autoRetry } else { update.autoRetry = false } } transactions.TransactionModel.findByIdAndUpdate(transactionId, update, {new: true}, (err, tx) => { if (err) { return callback(err) } callback(null, tx) // queue for autoRetry if (update.autoRetry) { autoRetryUtils.queueForRetry(tx) } // Asynchronously record transaction metrics metrics.recordTransactionMetrics(tx).catch(err => { logger.error('Recording transaction metrics failed', err) }) }) }) } export async function koaMiddleware (ctx, next) { const saveTransaction = promisify(storeTransaction) await saveTransaction(ctx) await next() storeResponse(ctx, () => { }) }