diff --git a/src/__tests__/retry.spec.ts b/src/__tests__/retry.spec.ts
new file mode 100644
index 0000000..4b761dd
--- /dev/null
+++ b/src/__tests__/retry.spec.ts
@@ -0,0 +1,105 @@
+import { describe, expect, test } from '@jest/globals'
+import axios from 'axios'
+import MockAdapter from 'axios-mock-adapter'
+
+import { CreateTemplate, TemplateVariables } from '../generated'
+import { PdfService } from '../services/PdfService'
+import { ScreenshotService } from '../services/ScreenshotService'
+import { TemplateService } from '../services/TemplateService'
+
+function createBody(): TemplateVariables {
+ return {
+ page: {
+ html: '
Hello
',
+ htmlTemplate: '{{name}}
',
+ templateData: { name: 'Example' }
+ },
+ pdf: {
+ headerHtml: '',
+ footerHtml: ''
+ }
+ }
+}
+
+function encodedBody(): TemplateVariables {
+ const body = createBody()
+ return {
+ page: {
+ ...body.page,
+ html: Buffer.from(body.page.html).toString('base64'),
+ htmlTemplate: Buffer.from(body.page.htmlTemplate).toString('base64')
+ },
+ pdf: {
+ headerHtml: Buffer.from(body.pdf.headerHtml).toString('base64'),
+ footerHtml: Buffer.from(body.pdf.footerHtml).toString('base64')
+ }
+ }
+}
+
+describe.each(['pdf', 'screenshot', 'template generation', 'template create', 'template update'])('%s encoding', service => {
+ function setup() {
+ const client = axios.create()
+ const mock = new MockAdapter(client)
+ const template = new TemplateService(client)
+ const requests: Record Promise> = {
+ pdf: body => new PdfService(client).direct(body),
+ screenshot: body => new ScreenshotService(client).direct(body),
+ 'template generation': body => template.direct('id', body),
+ 'template create': body => template.create({ name: 'Example', output: CreateTemplate.output.PDF, variables: body }),
+ 'template update': body => template.update('id', { variables: body })
+ }
+ return { mock, request: requests[service] }
+ }
+
+ function expectEncodedPayload(data: string) {
+ const payload = JSON.parse(data)
+ expect(payload.variables ?? payload).toEqual(encodedBody())
+ }
+
+ test('sends the same singly encoded content after a 429', async () => {
+ const { mock, request } = setup()
+ mock.onPost().replyOnce(429).onPost().reply(200, Buffer.from('result'))
+ const body = createBody()
+
+ await request(body)
+
+ expect(mock.history.post).toHaveLength(2)
+ expectEncodedPayload(mock.history.post[0].data)
+ expect(mock.history.post[1].data).toBe(mock.history.post[0].data)
+ expect(body).toEqual(encodedBody())
+ })
+
+ test('encodes the input in place on a successful call', async () => {
+ const { mock, request } = setup()
+ mock.onPost().reply(200, Buffer.from('result'))
+ const body = createBody()
+
+ await request(body)
+
+ expect(mock.history.post).toHaveLength(1)
+ expectEncodedPayload(mock.history.post[0].data)
+ expect(body).toEqual(encodedBody())
+ })
+
+ test('stops after one retry if the rate limit persists', async () => {
+ const { mock, request } = setup()
+ mock.onPost().reply(429)
+ const body = createBody()
+
+ await expect(request(body)).rejects.toMatchObject({ response: { status: 429 } })
+
+ expect(mock.history.post).toHaveLength(2)
+ expectEncodedPayload(mock.history.post[0].data)
+ expect(mock.history.post[1].data).toBe(mock.history.post[0].data)
+ expect(body).toEqual(encodedBody())
+ })
+
+ test('does not retry other errors', async () => {
+ const { mock, request } = setup()
+ mock.onPost().reply(500)
+
+ await expect(request(createBody())).rejects.toMatchObject({ response: { status: 500 } })
+
+ expect(mock.history.post).toHaveLength(1)
+ })
+})
diff --git a/src/services/BaseService.ts b/src/services/BaseService.ts
index 54424fc..8b91d7c 100644
--- a/src/services/BaseService.ts
+++ b/src/services/BaseService.ts
@@ -33,14 +33,18 @@ export class BaseService {
constructor(private readonly client: Axios) {}
- protected async request(method: Method, url: string, requestBody?: object, config: AxiosRequestConfig = {}, retries = 1): Promise {
+ protected async request(method: Method, url: string, requestBody?: object, config: AxiosRequestConfig = {}, retries = 1, encodeBody = true): Promise {
try {
await this.waitForRateLimit()
+ if (requestBody && encodeBody) {
+ requestBody = this.encodeRequestBody(requestBody)
+ }
+
const axiosResponse = await this.client.request({
method,
url: url,
- data: requestBody ? this.encodeRequestBody(requestBody) : undefined,
+ data: requestBody,
...config
})
this.processRateLimit(new AxiosHeaders(axiosResponse.headers))
@@ -52,7 +56,7 @@ export class BaseService {
return axiosResponse.data
} catch (err) {
if (isAxiosError(err) && err.status === 429 && retries > 0) {
- return this.request(method, url, requestBody, config, retries - 1)
+ return this.request(method, url, requestBody, config, retries - 1, false)
}
throw err
@@ -60,7 +64,7 @@ export class BaseService {
}
protected encodeRequestBody(requestBody: object): object {
- if (this.isGenerateRequest(requestBody)) {
+ if (this.isGenerateRequest(requestBody) && requestBody.page) {
if (requestBody.page.html) {
requestBody.page.html = this.baseEncodeContent(requestBody.page.html)
}
@@ -70,7 +74,7 @@ export class BaseService {
}
}
- if (this.isPdfRequest(requestBody)) {
+ if (this.isPdfRequest(requestBody) && requestBody.pdf) {
if (requestBody.pdf?.headerHtml) {
requestBody.pdf.headerHtml = this.baseEncodeContent(requestBody.pdf.headerHtml)
}