import {StatusCode} from '../types'; import { abstractConvertResponse, AdapterResponse, isOK, NormalizedResponse, } from '../../runtime/http'; import {ConfigInterface} from '../base-types'; import * as ShopifyErrors from '../error'; import {logger} from '../logger'; import { DeliveryMethod, HttpWebhookHandlerWithCallback, WebhookProcessParams, WebhookRegistry, WebhookType, WebhookValidationErrorReason, WebhookValidationInvalid, WebhookValidationMissingHeaders, WebhookValidationValid, } from './types'; import {validateFactory} from './validate'; interface HandlerCallResult { statusCode: StatusCode; errorMessage?: string; } interface ErrorCallResult { statusCode: StatusCode; errorMessage: string; } const STATUS_TEXT_LOOKUP: Record = { [StatusCode.Ok]: 'OK', [StatusCode.BadRequest]: 'Bad Request', [StatusCode.Unauthorized]: 'Unauthorized', [StatusCode.NotFound]: 'Not Found', [StatusCode.InternalServerError]: 'Internal Server Error', }; export function process( config: ConfigInterface, webhookRegistry: WebhookRegistry, ) { return async function process({ context, rawBody, ...adapterArgs }: WebhookProcessParams): Promise { const response: NormalizedResponse = { statusCode: StatusCode.Ok, statusText: STATUS_TEXT_LOOKUP[StatusCode.Ok], headers: {}, }; await logger(config).info('Receiving webhook request'); const webhookCheck = await validateFactory(config)({ rawBody, ...adapterArgs, }); let errorMessage = 'Unknown error while handling webhook'; if (webhookCheck.valid) { const handlerResult = await callWebhookHandlers( config, webhookRegistry, webhookCheck, rawBody, context, ); response.statusCode = handlerResult.statusCode; if (!isOK(response)) { errorMessage = handlerResult.errorMessage || errorMessage; } } else { const errorResult = await handleInvalidWebhook(config, webhookCheck); response.statusCode = errorResult.statusCode; response.statusText = STATUS_TEXT_LOOKUP[response.statusCode]; errorMessage = errorResult.errorMessage; } const returnResponse = await abstractConvertResponse(response, adapterArgs); if (!isOK(response)) { throw new ShopifyErrors.InvalidWebhookError({ message: errorMessage, response: returnResponse, }); } return Promise.resolve(returnResponse); }; } async function callWebhookHandlers( config: ConfigInterface, webhookRegistry: WebhookRegistry, webhookCheck: WebhookValidationValid, rawBody: string, context: any, ): Promise { const log = logger(config); const {hmac: _hmac, valid: _valid, ...loggingContext} = webhookCheck; await log.debug( 'Webhook request is valid, looking for HTTP handlers to call', loggingContext, ); const handlers = webhookRegistry[webhookCheck.topic] || []; const response: HandlerCallResult = {statusCode: StatusCode.Ok}; let found = false; for (const handler of handlers) { if (handler.deliveryMethod !== DeliveryMethod.Http) { continue; } if (!handler.callback) { response.statusCode = StatusCode.InternalServerError; response.errorMessage = "Cannot call webhooks.process with a webhook handler that doesn't have a callback"; throw new ShopifyErrors.MissingWebhookCallbackError({ message: response.errorMessage, response, }); } found = true; await log.debug('Found HTTP handler, triggering it', loggingContext); // process() only handles programmatically-registered HTTP webhooks; // events are registered via app TOML and don't use this code path. if (webhookCheck.webhookType !== WebhookType.Webhooks) { throw new ShopifyErrors.InvalidWebhookError({ message: 'process() only supports traditional webhooks, not events', response, }); } const {webhookId, subTopic} = webhookCheck; try { await handler.callback( webhookCheck.topic, webhookCheck.domain, rawBody, webhookId, webhookCheck.apiVersion, ...(subTopic ? subTopic : ''), context, ); } catch (error) { response.statusCode = StatusCode.InternalServerError; response.errorMessage = error.message; } } if (!found) { await log.debug('No HTTP handlers found', loggingContext); response.statusCode = StatusCode.NotFound; response.errorMessage = `No HTTP webhooks registered for topic ${webhookCheck.topic}`; } return response; } async function handleInvalidWebhook( config: ConfigInterface, webhookCheck: WebhookValidationInvalid, ): Promise { const response: ErrorCallResult = { statusCode: StatusCode.InternalServerError, errorMessage: 'Unknown error while handling webhook', }; switch (webhookCheck.reason) { case WebhookValidationErrorReason.MissingHeaders: response.statusCode = StatusCode.BadRequest; response.errorMessage = `Missing one or more of the required HTTP headers to process webhooks: [${( webhookCheck as WebhookValidationMissingHeaders ).missingHeaders.join(', ')}]`; break; case WebhookValidationErrorReason.MissingBody: response.statusCode = StatusCode.BadRequest; response.errorMessage = 'No body was received when processing webhook'; break; case WebhookValidationErrorReason.MissingHmac: response.statusCode = StatusCode.BadRequest; response.errorMessage = `Missing HMAC header in request`; break; case WebhookValidationErrorReason.InvalidHmac: response.statusCode = StatusCode.Unauthorized; response.errorMessage = `Could not validate request HMAC`; break; } await logger(config).debug( `Webhook request is invalid, returning ${response.statusCode}: ${response.errorMessage}`, ); return response; }