{"id":"11de973a30be7905","repo":"sindresorhus/got","slug":"a-retry-listener-has-been-attached-already","errorCode":null,"errorMessage":"A retry listener has been attached already.","messagePattern":"A retry listener has been attached already\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"source/core/index.ts","lineNumber":384,"sourceCode":"\t\t\t\tfor (const [header, value] of Object.entries(source.headers)) {\n\t\t\t\t\tconst normalizedHeader = header.toLowerCase();\n\n\t\t\t\t\tif (omittedPipedHeaders.has(normalizedHeader) || connectionListedHeaders.has(normalizedHeader)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!this.options.shouldCopyPipedHeader(normalizedHeader)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.options.setPipedHeader(normalizedHeader, value);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tthis.on('newListener', event => {\n\t\t\tif (event === 'retry' && this.listenerCount('retry') > 0) {\n\t\t\t\tthrow new Error('A retry listener has been attached already.');\n\t\t\t}\n\t\t});\n\n\t\ttry {\n\t\t\tthis.options = new Options(url, options, defaults);\n\n\t\t\tif (!this.options.url) {\n\t\t\t\tif (this.options.prefixUrl === '') {\n\t\t\t\t\tthrow new TypeError('Missing `url` property');\n\t\t\t\t}\n\n\t\t\t\tthis.options.url = '';\n\t\t\t}\n\n\t\t\tthis.requestUrl = this.options.url as URL;\n\n\t\t\t// Publish request creation event\n\t\t\tpublishRequestCreate({","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/sindresorhus/got/blob/e3924aa1e53a6ca3eb93a43618ce532442a89b40/source/core/index.ts#L366-L402","documentation":"Thrown at source/core/index.ts:384 from a `newListener` event handler on the Request stream. The Request reserves the `retry` event for its own internal use — the promise wrapper (as-promise) attaches a single retry listener to drive makeRequest. If a second listener for `retry` is added, got throws immediately because the retry orchestration assumes exactly one consumer; duplicate listeners would cause duplicate retry attempts or double-makeRequest calls.","triggerScenarios":"Calling `request.on('retry', ...)` yourself on a Request object that got already manages, or attaching two retry listeners. Most commonly hit by users of the stream API (got.stream) who try to hook retry manually, or by code that forwards events from one request to another and inadvertently forwards `retry` too.","commonSituations":"Custom event-forwarding wrappers, proxying all events from an inner request to an outer emitter (the library itself uses a guarded proxiedRequestEvents list that deliberately omits `retry`), or older tutorials that predate the promise-based retry API advising `.on('retry', ...)`.","solutions":["Do not attach your own `retry` listener — use the `retry` option (retry.limit, retry.statusCodes, retry.methods) and the `afterResponse` retry callback instead.","If you proxy events between emitters, exclude `retry` from the forwarded event list (got's own proxiedRequestEvents at as-promise/index.ts:30 is the reference list).","Use the hooks API (hooks.beforeRetry, hooks.afterResponse) for any retry-side logic you need."],"exampleFix":"// before\nconst stream = got.stream('https://api');\nstream.on('retry', (count, error) => console.log('retrying', count));\n\n// after — use the retry hook instead\nawait got('https://api', {\n  retry: { limit: 3 },\n  hooks: { beforeRetry: [(options, error, retryCount) => console.log('retrying', retryCount)] }\n});","handlingStrategy":"validation","validationCode":"// Ensure you never attach your own 'retry' listener. Use hooks instead.\nconst forbidden = ['retry'];\nfunction assertNoInternalListeners(request, event) {\n  if (forbidden.includes(event)) {\n    throw new Error(`Do not attach '${event}' on a got Request — use the retry option or hooks.beforeRetry.`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  request.on('retry', handler);\n} catch (error) {\n  if (error instanceof Error && /retry listener has been attached already/.test(error.message)) {\n    throw new Error('Use hooks.beforeRetry or the retry option instead of request.on(\"retry\", ...)', { cause: error });\n  }\n  throw error;\n}","preventionTips":["Never call request.on('retry', ...); configure retry via the `retry` option and hooks.beforeRetry.","When proxying events between emitters, exclude `retry` (got's proxiedRequestEvents list is the reference).","Use the promise API for retry-driven flows."],"tags":["events","retry","stream-api","contract-violation"],"analyzedSha":"e3924aa1e53a6ca3eb93a43618ce532442a89b40","analyzedAt":"2026-08-03T19:22:24.770Z","schemaVersion":2}