Skip to content

Commit

Permalink
PLU-125: [EXCEL-12] After request checks (e.g. HTTP 429) (#375)
Browse files Browse the repository at this point in the history
  • Loading branch information
ogp-weeloong authored Jan 8, 2024
1 parent 9115115 commit df41a62
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { IApp } from '@plumber/types'

import logger from '@/helpers/logger'

const http429Handler: IApp['requestErrorHandler'] = async function ($, error) {
if (error.response.status !== 429) {
return
}

// A 429 response is considered a SEV-2+ incident for some tenants; log it
// explicitly so that we can easily trigger incident creation from DD.
logger.error('Received HTTP 429 from MS Graph', {
event: 'm365-http-429',
tenant: $.auth?.data?.tenantKey as string,
baseUrl: error.response.config.baseURL,
url: error.response.config.url,
flowId: $.flow?.id,
stepId: $.step?.id,
executionId: $.execution?.id,
})

// We don't want to retry 429s from M365, so convert it into a non-HttpError.
throw new Error('Rate limited by Microsoft Graph.')
}

export default http429Handler
2 changes: 2 additions & 0 deletions packages/backend/src/apps/m365-excel/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IApp } from '@plumber/types'

import beforeRequest from './common/interceptors/before-request'
import requestErrorHandler from './common/interceptors/request-error-handler'
import actions from './actions'
import auth from './auth'
import dynamicData from './dynamic-data'
Expand All @@ -16,6 +17,7 @@ const app: IApp = {
auth,
actions,
beforeRequest,
requestErrorHandler,
dynamicData,
}

Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/helpers/global-variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ const globalVariable = async (
$.http = createHttpClient({
$,
baseURL: app.apiBaseUrl,
beforeRequest: app.beforeRequest,
beforeRequest: app.beforeRequest ?? [],
requestErrorHandler: app.requestErrorHandler ?? null,
})

if (flow) {
Expand Down
23 changes: 22 additions & 1 deletion packages/backend/src/helpers/http-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const removeBaseUrlForAbsoluteUrls = (
export default function createHttpClient({
$,
baseURL,
beforeRequest = [],
beforeRequest,
requestErrorHandler,
}: IHttpClientParams) {
const instance = axios.create({
baseURL,
Expand Down Expand Up @@ -90,6 +91,26 @@ export default function createHttpClient({
throw new HttpError(error)
},
)
// We use a separate interceptor for requestErrorHandler to allow the above
// HttpError inteceptor to throw early.
instance.interceptors.response.use(
(response) => response,
async (error) => {
if (!requestErrorHandler) {
throw error
}

// Passthrough other errors... although other errors should really only be
// RetriableError.
if (!(error instanceof HttpError)) {
throw error
}

await requestErrorHandler($, error)

throw error
},
)

return instance
}
33 changes: 26 additions & 7 deletions packages/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import HttpError from '@/errors/http'
import type {
AxiosInstance,
AxiosRequestConfig,
Expand Down Expand Up @@ -273,15 +274,32 @@ export interface IApp {
actions?: IAction[]
connections?: IConnection[]
description?: string
}

export type TBeforeRequest = {
(
$: IGlobalVariable,
requestConfig: InternalAxiosRequestConfig,
): Promise<InternalAxiosRequestConfig>
/**
* A callback that is invoked if there's an error for any HTTP request this
* app makes using $.http.
*
* This is useful to perform per-request monitoring or error transformations
* (e.g logging on specific HTTP response codes or converting 429s to a
* non-HttpError to prevent automated retries).
*
* We support this because if an app needs custom error monitoring for _all_
* requests, it allows us to stop having to remember to surround all our code
* with try / catch.
*/
requestErrorHandler?: TRequestErrorHandler
}

export type TBeforeRequest = (
$: IGlobalVariable,
requestConfig: InternalAxiosRequestConfig,
) => Promise<InternalAxiosRequestConfig>

export type TRequestErrorHandler = (
$: IGlobalVariable,
error: HttpError,
) => Promise<void>

export interface DynamicDataOutput {
data: {
name: string
Expand Down Expand Up @@ -473,7 +491,8 @@ export interface ISubstep {
export type IHttpClientParams = {
$: IGlobalVariable
baseURL?: string
beforeRequest?: TBeforeRequest[]
beforeRequest: TBeforeRequest[]
requestErrorHandler: TRequestErrorHandler
}

export type IGlobalVariable = {
Expand Down

0 comments on commit df41a62

Please sign in to comment.