Skip to content

Handle API-disabled 404 Reason header across SDKs#238

Open
robzolkos wants to merge 4 commits intobasecamp:mainfrom
robzolkos:handle-api-disabled-reason-header
Open

Handle API-disabled 404 Reason header across SDKs#238
robzolkos wants to merge 4 commits intobasecamp:mainfrom
robzolkos:handle-api-disabled-reason-header

Conversation

@robzolkos
Copy link
Copy Markdown
Collaborator

@robzolkos robzolkos commented Mar 30, 2026

Summary

This PR updates the SDKs to recognize the upcoming BC3 behavior from bc3#10136, where public API access can be disabled at the account level and blocked requests return 404 Not Found with Reason: API Disabled.

Without this change, SDK consumers would see a generic not-found error, which obscures the real account-level condition and makes remediation unclear. This PR surfaces that response as a distinct SDK error/code with a helpful hint, while preserving existing source compatibility in Swift and Kotlin for consumers using exhaustive error matching.

What this does

  • Detects Reason: API Disabled on 404 responses across the SDKs
  • Maps that response to an explicit api_disabled error/code and exit code 10
  • Preserves the existing Account Inactive handling for 404 responses with Reason: Account Inactive
  • Carries through request IDs where the SDKs already expose them
  • Updates SDK docs and tests to cover the new behavior

Compatibility notes

  • Go, TypeScript, Python, and Ruby expose the API-disabled condition as a distinct error/code in their existing error models.
  • Swift and Kotlin preserve source compatibility by continuing to surface the response as the existing not-found variant/type, while exposing:
    • code == "api_disabled"
    • the appropriate exit code
    • an API-disabled convenience flag/property
  • Swift also now performs case-insensitive header lookup for Reason and Retry-After, which makes this handling more robust across transports and header representations.

User-visible behavior

When an account administrator disables public API access, SDK consumers now get an actionable error instead of a generic not-found response, with guidance that API access can be re-enabled in Adminland under Manage API access.


Summary by cubic

Surfaces 404s with Reason: API Disabled as a distinct api_disabled error with exit code 10 across all SDKs, with a clear Adminland hint. Also recognizes Account Inactive on 404s, and preserves Kotlin binary compatibility.

  • New Features

    • Detects 404 + Reason: API Disabled and maps to code api_disabled with exit code 10 and guidance to re-enable in Adminland.
    • Handles Reason: Account Inactive as not_found with a clear message and hint.
    • Preserves request IDs on these errors; Swift now performs case-insensitive lookup for Reason and Retry-After.
    • Kotlin maps the Reason header, exposes an isApiDisabled flag, and retains legacy NotFound constructors for binary compatibility.
    • Updates docs and tests across Go, Kotlin, Python, Ruby, Swift, and TypeScript.
  • Migration

    • No breaking changes. Swift/Kotlin still surface NotFound for 404s; when Reason is API Disabled, code == "api_disabled" and exit code is 10. Kotlin keeps legacy NotFound constructors.
    • Other SDKs expose a distinct ApiDisabled error/code. If you special-case generic 404s, handle api_disabled explicitly.

Written for commit 0779ef2. Summary will update on new commits.

Copilot AI review requested due to automatic review settings March 30, 2026 12:36
@github-actions github-actions bot added typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK go kotlin swift bug Something isn't working labels Mar 30, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Go/TypeScript/Swift/Kotlin/Python/Ruby SDKs to detect Basecamp’s upcoming “public API disabled” account-level behavior (404 Not Found with Reason: API Disabled) and surface it as a distinct SDK error code (api_disabled) with exit code 10, while preserving existing Reason: Account Inactive handling.

Changes:

  • Map 404 + Reason: API Disabled to an api_disabled error/code (exit code 10) across SDKs (with Swift/Kotlin preserving exhaustive-match compatibility).
  • Preserve/extend request-id propagation where supported, and improve Swift header lookup robustness (case-insensitive).
  • Update docs and add tests in each SDK to cover the new behavior.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
typescript/tests/errors.test.ts Adds tests for api_disabled exit code mapping and Reason-based 404 behavior.
typescript/src/errors.ts Introduces api_disabled code/exit code and maps 404 + Reason to explicit errors.
typescript/README.md Documents the new api_disabled error code and exit code.
swift/Tests/BasecampTests/ErrorTests.swift Adds assertions and new tests for API-disabled and case-insensitive header handling.
swift/Sources/Basecamp/BasecampError.swift Adds stable code, isAPIDisabled, exit code 10 mapping, and case-insensitive header lookup for Reason / Retry-After.
swift/README.md Documents Swift compatibility approach: .notFound with code == "api_disabled".
ruby/test/basecamp/http_test.rb Adds HTTP-layer tests for Reason: API Disabled and Reason: Account Inactive.
ruby/test/basecamp/errors_test.rb Adds error model tests for ApiDisabledError, exit code 10, and error_from_response mapping.
ruby/lib/basecamp/http.rb Maps 404 + Reason to ApiDisabledError / account-inactive NotFoundError in HTTP error handling.
ruby/lib/basecamp/exit_code.rb Adds API_DISABLED = 10.
ruby/lib/basecamp/error_code.rb Adds API_DISABLED = "api_disabled".
ruby/lib/basecamp/error.rb Maps api_disabled to exit code 10.
ruby/lib/basecamp/client.rb Passes response headers into Basecamp.error_from_response in a defensive fallback path.
ruby/lib/basecamp/api_disabled_error.rb Introduces ApiDisabledError with message/hint/http status defaults.
ruby/lib/basecamp.rb Extends error_from_response to accept headers and map Reason for 404s.
python/tests/test_http.py Adds HTTP client tests mapping Reason: API Disabled/Account Inactive.
python/tests/test_errors.py Adds unit tests for ApiDisabledError, exit code 10, and error_from_response mapping.
python/src/basecamp/errors.py Adds ApiDisabledError, error/exit codes, and Reason-based 404 mapping.
python/src/basecamp/init.py Exports ApiDisabledError.
python/README.md Documents ApiDisabledError and api_disabled code.
kotlin/sdk/src/commonTest/kotlin/com/basecamp/sdk/ErrorTest.kt Adds tests for 404 reason mapping and isApiDisabled behavior.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/services/BaseService.kt Passes Reason header through to exception mapping.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/Download.kt Passes Reason header through for download error mapping.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampException.kt Adds CODE_API_DISABLED, exit code 10, isApiDisabled, and 404 reason mapping.
kotlin/README.md Documents Kotlin compatibility approach: NotFound with code == CODE_API_DISABLED.
go/pkg/basecamp/helpers_test.go Adds tests for Reason-based mapping and request-id preservation.
go/pkg/basecamp/helpers.go Adds checkReasonHeader and applies it to 404 handling in checkResponse.
go/pkg/basecamp/errors_test.go Adds tests for ErrAPIDisabled, ErrAccountInactive, and exit code mapping.
go/pkg/basecamp/errors.go Adds CodeAPIDisabled, exit code 10, and constructors for API-disabled / account-inactive errors.
go/pkg/basecamp/client.go Applies Reason-based mapping to 404 handling in the manual HTTP client path.
go/README.md Documents the new api_disabled error code and exit code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread go/pkg/basecamp/client.go
Comment thread ruby/lib/basecamp.rb
Comment thread swift/Sources/Basecamp/BasecampError.swift Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 31 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="ruby/lib/basecamp.rb">

<violation number="1" location="ruby/lib/basecamp.rb:92">
P3: `Basecamp.error_from_response` now receives the raw headers but still discards `X-Request-Id`, so errors raised from `download_url` never expose the request ID even though `Basecamp::Error` supports one. Parse the request ID from the headers (handling both casings) and assign it to the error before returning, matching the behavior of `Http#handle_error`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread ruby/lib/basecamp.rb
@robzolkos
Copy link
Copy Markdown
Collaborator Author

Fixed — preserved X-Request-Id on Go/Ruby reason-based 404 errors and tightened Swift api_disabled detection to require both the internal message and hint.

@robzolkos
Copy link
Copy Markdown
Collaborator Author

Fixed issue identified by cubic — Basecamp.error_from_response now preserves X-Request-Id from either header casing.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampException.kt Outdated
@github-actions github-actions bot added enhancement New feature or request and removed bug Something isn't working labels Mar 30, 2026
…active

The Basecamp API returns a Reason header on 404 responses to distinguish
account-level conditions from regular not-found errors:

- "Reason: API Disabled" — an administrator has turned off API access
- "Reason: Account Inactive" — expired trial or suspended account

Previously both cases returned a generic "resource not found" error,
which was confusing for developers and end users.

Now both checkResponse (used by service methods via the generated client)
and the raw client's 404 handler inspect the Reason header and return
specific, actionable errors:

- ErrAPIDisabled() with CodeAPIDisabled and a hint to contact an admin
- ErrAccountInactive() with CodeNotFound and a hint about the account state

Added CodeAPIDisabled error code and ExitAPIDisabled (exit code 10) so
CLI tools can programmatically detect this condition.
Copilot AI review requested due to automatic review settings March 30, 2026 20:08
@robzolkos robzolkos force-pushed the handle-api-disabled-reason-header branch from 2541944 to 0779ef2 Compare March 30, 2026 20:08
@github-actions github-actions bot added bug Something isn't working and removed enhancement New feature or request labels Mar 30, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Labels

bug Something isn't working go kotlin ruby Pull requests that update the Ruby SDK swift typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants