diff --git a/lib/httpBlock.ts b/lib/httpBlock.ts index 4883d0b..0787169 100644 --- a/lib/httpBlock.ts +++ b/lib/httpBlock.ts @@ -103,6 +103,10 @@ export interface DescriptHttpBlockDescription< servername?: DescriptHttpBlockDescriptionCallback; timeout?: DescriptHttpBlockDescriptionCallback; + // Отдельный таймаут на чтение ответа. Если не указан, то используется общий таймаут. + // Если указан, то после получения первого байта из ответа общий таймаут сбрасывается и выставляется новый только на чтение. + readTimeout?: DescriptHttpBlockDescriptionCallback; + query?: HttpQuery | Array>; headers?: HttpHeaders | Array>; @@ -134,6 +138,7 @@ const EVALUABLE_PROPS: Array; cancel: Cancel; timestamps: EventTimestamps; - hTimeout: number | null; + hTimeout: NodeJS.Timeout | null; req?: http.ClientRequest; isResolved: boolean; @@ -466,8 +469,9 @@ class DescriptRequest { this.doFail(error); } - setTimeout() { - if ((this.options.timeout || 0) > 0) { + setTimeout(customTimeout?: number) { + const timeout = customTimeout || this.options.timeout || 0; + if (timeout > 0) { this.hTimeout = setTimeout(() => { let error; if (!this.timestamps.tcpConnection) { @@ -480,7 +484,7 @@ class DescriptRequest { this.doCancel(error); - }, this.options.timeout) as unknown as number; + }, timeout); } } @@ -500,6 +504,13 @@ class DescriptRequest { async requestHandler(res: http.IncomingMessage): Promise { res.once('readable', () => { this.timestamps.firstByte = Date.now(); + + // Если есть отдельный таймаут на чтение, то после получения первого байта, + // нужно сбросить общий таймаут и создать новый. + if (this.options.readTimeout) { + this.clearTimeout(); + this.setTimeout(this.options.readTimeout); + } }); const unzipped = decompressResponse(res); diff --git a/tests/request.test.ts b/tests/request.test.ts index a386d61..58b6a9f 100644 --- a/tests/request.test.ts +++ b/tests/request.test.ts @@ -715,6 +715,48 @@ describe('request', () => { } }); + it('200, timeout, readTimeout, slow response', async() => { + const path = getPath(); + + fake.add(path, async(req: http.IncomingMessage, res: http.ServerResponse) => { + res.statusCode = 200; + res.write('Привет!'); + await waitForValue(null, 100); + res.end(); + }); + + const result = await doRequest({ + pathname: path, + timeout: 50, + readTimeout: 150, + }); + expect(result.body?.toString()).toBe('Привет!'); + }); + + it('200, timeout, readTimeout, incomplete response', async() => { + const path = getPath(); + + fake.add(path, async(req: http.IncomingMessage, res: http.ServerResponse) => { + res.statusCode = 200; + res.write('Привет!'); + await waitForValue(null, 100); + res.end(); + }); + + expect.assertions(2); + try { + await doRequest({ + pathname: path, + timeout: 50, + readTimeout: 90, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.REQUEST_TIMEOUT); + } + }); + }); describe('content-encoding', () => {