{"record":{"id":"70765b7c9ff637ee","repo":"serverless/serverless","slug":"api-gateway-custom-domain-deletion-failed","errorCode":"API_GATEWAY_CUSTOM_DOMAIN_DELETION_FAILED","errorMessage":"V1 - Failed to delete custom domain '${domain.givenDomainName}':\\n${err.message}","messagePattern":"V1 - Failed to delete custom domain '(.+?)':\\\\n(.+?)","errorType":"exception","errorClass":"ServerlessError","httpStatus":null,"severity":"error","filePath":"packages/serverless/lib/plugins/aws/domains/aws/api-gateway-v1-wrapper.js","lineNumber":128,"sourceCode":"          `V1 - Unable to fetch information about '${domain.givenDomainName}':\\n${err.message}`,\n          ServerlessErrorCodes.domains.API_GATEWAY_CUSTOM_DOMAIN_FETCH_FAILED,\n          { originalMessage: err.message },\n        )\n      }\n      Logging.logWarning(`V1 - '${domain.givenDomainName}' does not exist.`)\n    }\n  }\n\n  async deleteCustomDomain(domain) {\n    // Make API call\n    try {\n      await this.apiGateway.send(\n        new DeleteDomainNameCommand({\n          domainName: domain.givenDomainName,\n        }),\n      )\n    } catch (err) {\n      throw new ServerlessError(\n        `V1 - Failed to delete custom domain '${domain.givenDomainName}':\\n${err.message}`,\n        ServerlessErrorCodes.domains.API_GATEWAY_CUSTOM_DOMAIN_DELETION_FAILED,\n        { originalMessage: err.message },\n      )\n    }\n  }\n\n  async updateCustomDomain(domain) {\n    const patchOperations = []\n\n    if (\n      domain.hasSecurityPolicyConfigured &&\n      domain.domainInfo?.securityPolicy !== domain.securityPolicy\n    ) {\n      patchOperations.push({\n        op: 'replace',\n        path: '/securityPolicy',\n        value: domain.securityPolicy,","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/serverless/serverless/blob/b9d7ea51c8cce57cff1207964b9b71123673081f/packages/serverless/lib/plugins/aws/domains/aws/api-gateway-v1-wrapper.js#L110-L146","documentation":"Thrown by APIGatewayV1Wrapper.deleteCustomDomain when DeleteDomainNameCommand is rejected. AWS refuses to delete a domain while it still has base path mappings, so this error frequently surfaces during teardown or domain migration. The wrapper preserves the AWS-side rejection message.","triggerScenarios":"DeleteDomainNameCommand against a domain that does not exist (404); a domain that still has base path mappings attached (400 TooManyRequests / ConflictException); missing apigateway:DELETE permission; concurrent delete from another deploy.","commonSituations":"sls remove ran out of order - base path mappings were not removed first; partial failed deploy left dangling mappings; CI re-running teardown against an already-deleted domain; cross-stack references keep the domain alive.","solutions":["Manually remove any base path mappings first: aws apigateway get-base-path-mapping --domain-name <dn> --base-path <bp>, then delete-base-path-mapping.","If the domain is already gone (404), the error is safe to ignore - clear it from customDomain config or run sls remove again.","Confirm apigateway:DELETE IAM permission is present.","Re-run sls remove after the mappings clear; do not retry the same command unchanged."],"exampleFix":"# manual cleanup before re-running sls remove\naws apigateway get-base-path-mappings --domain-name api.example.com\naws apigateway delete-base-path-mapping --domain-name api.example.com --base-path '(none)'\n# then\nsls remove","handlingStrategy":"validation","validationCode":"// Before deleteCustomDomain, drain base path mappings first\nasync function safeDeleteDomain(wrapper, domain) {\n  let mappings = []\n  try { mappings = await wrapper.getBasePathMappings(domain) } catch {}\n  for (const m of mappings) {\n    domain.apiMapping = m\n    await wrapper.deleteBasePathMapping(domain)\n  }\n  await wrapper.deleteCustomDomain(domain)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await wrapper.deleteCustomDomain(domain)\n} catch (err) {\n  const msg = err.cause?.originalMessage || err.message\n  if (err.code === 'API_GATEWAY_CUSTOM_DOMAIN_DELETION_FAILED') {\n    if (/NotFound|not found/i.test(msg)) return // already gone - benign\n    if (/mappings|Conflict|still/i.test(msg))\n      throw new Error('drain base path mappings before delete')\n    throw err\n  }\n  throw err\n}","preventionTips":["Always remove base path mappings before deleting the domain.","Make teardown idempotent - tolerate 404 on the domain.","Grant apigateway:DELETE on /domainnames/*.","Avoid running two teardowns concurrently on the same domain."],"tags":["aws","api-gateway","custom-domain","cleanup","iam"],"backgroundTag":null,"analyzedSha":"b9d7ea51c8cce57cff1207964b9b71123673081f","analyzedAt":"2026-08-13T04:14:40.386Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}