Skip to content

fix(gmail): correctly normalize Google API 400 403 and 429 errors - #39

Open
kjxcodez wants to merge 1 commit into
mainfrom
fix/gmail-api-error-normalization
Open

kjxcodez wants to merge 1 commit into
mainfrom
fix/gmail-api-error-normalization

Conversation

@kjxcodez

@kjxcodez kjxcodez commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Problem

The forensic audit (Issue #24, PR #31) identified three critical error-mapping defects in apps/api/src/services/google/gmail.provider.ts:

  1. HTTP 400 Bad Request: Malformed MIME, RFC 2822 header length exceeded, or invalid characters were hardcoded to INVALID_RECIPIENT, triggering false-positive permanent suppression of valid contacts as HARD_BOUNCE.
  2. HTTP 403 Forbidden: Daily sending quota exhaustion and Google bulk sender/anti-abuse filter blocks were hardcoded to MAILBOX_REAUTH_REQUIRED, mutating GoogleConnectionModel and EmailAccountModel to reauth_required and falsely instructing users to reconnect their OAuth authorization when their credentials were valid.
  3. HTTP 429 Too Many Requests: Threw SENDER_RATE_LIMITED with rate_limit, while downstream EmailService.send() checked for PROVIDER_RATE_LIMITED and provider_rate_limited, silently bypassing mailbox rate-limit cooldown.

Root Cause

  1. GmailProvider.sendMessage() inspected only HTTP status codes (400, 401/403, 429) without examining Google API response error structures (body.error.message, body.error.status, body.error.errors[].reason).
  2. Error token divergence between GmailProvider (SENDER_RATE_LIMITED) and EmailService.send() (PROVIDER_RATE_LIMITED).
  3. classifyEmailFailure() lacked dedicated classification cases for provider payload formatting errors (MALFORMED_PAYLOAD) and provider anti-abuse blocks (POLICY_BLOCKED).

Changes

  1. apps/api/src/services/email/types.ts:
    • Added 'MALFORMED_PAYLOAD', 'POLICY_BLOCKED', and 'QUOTA_EXCEEDED' to EmailProviderErrorShape['code'].
  2. apps/api/src/services/google/gmail.provider.ts:
    • HTTP 400: Inspects error text and reasons. If explicitly a recipient error, throws INVALID_RECIPIENT; otherwise throws MALFORMED_PAYLOAD (classification: 'malformed_payload').
    • HTTP 403: Inspects error payload.
      • If quota/limit (dailyLimitExceeded, userRateLimitExceeded, quota message): throws PROVIDER_RATE_LIMITED with parsed Retry-After delay without mutating connection status.
      • If policy/abuse (abuse, spam, policyRejection, bulk sending messages): throws POLICY_BLOCKED without mutating connection status.
      • If genuine auth/scope failure: updates GoogleConnectionModel to reauth_required and throws MAILBOX_REAUTH_REQUIRED.
    • HTTP 429: Throws PROVIDER_RATE_LIMITED (classification: 'provider_rate_limited') with parsed Retry-After delay.
  3. apps/api/src/services/email/email.service.ts:
    • In classifyEmailFailure():
      • Mapped MALFORMED_PAYLOAD to EmailFailureCategory.INTERNAL (isHardBounce: false, non-retryable).
      • Mapped POLICY_BLOCKED to EmailFailureCategory.POLICY (isHardBounce: false, non-retryable).
      • Mapped PROVIDER_RATE_LIMITED / QUOTA_EXCEEDED to EmailFailureCategory.RATE_LIMIT (isHardBounce: false, retryable).
    • In send():
      • Aligned cooldown triggering to handle PROVIDER_RATE_LIMITED, SENDER_RATE_LIMITED, QUOTA_EXCEEDED, and RATE_LIMIT failures.
      • Explicitly protected MALFORMED_PAYLOAD from triggering contact suppression.
  4. apps/api/src/middleware/error-handler.ts:
    • Added POLICY_BLOCKED to HTTP 403 status map.
  5. Tests:
    • Added apps/api/src/services/google/gmail-error-normalization.test.ts (10 tests covering 400 payload vs recipient, 403 quota vs abuse vs auth, 429 rate limit, and suppression prevention).
    • Updated apps/api/src/services/email/outbound-provider-rejection-audit.test.ts Finding 2 tests to verify remediated behavior.

Safety Impact

  • Eliminates false-positive suppression of valid contacts on message formatting, MIME, or RFC 2822 header errors.
  • Prevents spurious mailbox disconnections and confusing re-authentication requests when Google quota or anti-abuse filters activate.
  • Ensures provider cooldown is invoked whenever Google returns HTTP 429 or quota limits.

Tests

  • pnpm --filter @leadforge/schema check-types: Clean (0 errors)
  • pnpm --filter api check-types: Clean (0 errors)
  • pnpm --filter api exec vitest run src/services/email/ src/services/google/: 6 test files passed (51 passed)

Verification

  • Verified that:
    • 400 payload errors throw MALFORMED_PAYLOAD, categorized as INTERNAL, and do not suppress contacts.
    • 400 recipient errors throw INVALID_RECIPIENT and trigger suppression.
    • 403 quota errors throw PROVIDER_RATE_LIMITED, do not set connection reauth_required, and activate mailbox cooldown.
    • 403 abuse/policy errors throw POLICY_BLOCKED and do not set connection reauth_required.
    • 403 scope/auth errors throw MAILBOX_REAUTH_REQUIRED and set connection reauth_required.
    • 429 errors normalize to PROVIDER_RATE_LIMITED and trigger mailbox cooldown.

Scope

Related Issue

Inspect Google REST API error payloads in GmailProvider.sendMessage(): disambiguate HTTP 400 malformed payloads from invalid recipients to prevent false-positive suppression; disambiguate HTTP 403 between daily sending quotas, abuse/spam policies, and genuine auth failures without false reauth mutation; align HTTP 429 tokens to PROVIDER_RATE_LIMITED to activate mailbox cooldown. Closes #33.
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
leadforge-os-api Ready Ready Preview Sep 9, 2026 9:01pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
leadforge-os-marketing Skipped Skipped Sep 9, 2026 9:01pm UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(gmail): correctly normalize Google API 400 403 and 429 errors

1 participant