feat(openapi): distinguish expired OAuth bearer from invalid token#38142
Open
GareArc wants to merge 1 commit into
Open
feat(openapi): distinguish expired OAuth bearer from invalid token#38142GareArc wants to merge 1 commit into
GareArc wants to merge 1 commit into
Conversation
Previously an expired OAuth bearer and an unknown/invalid one both surfaced as an indistinguishable generic 401 (and an invalid token actually leaked a 500), so a client could not tell "session expired, re-authenticate" apart from "never authenticated." The resolver now raises a distinct TokenExpiredError for expired DB rows and records a separate `expired` negative-cache marker, so a retry within the negative-cache TTL still reports expiry instead of collapsing into a generic miss. The auth pipeline maps the two domain errors to unified OpenApiError responses: SessionExpired (code `token_expired`) and InvalidBearer (code `unauthorized`), both 401. This also fixes the latent 500 on invalid bearers. The `token_expired` code is synced through the contract codegen into the generated types/zod, and the difyctl error mapper branches the 401 on it. The CLI `expired_token` taxonomy member (RFC 8628 device-flow code expiry) is merged into `token_expired`; the RFC 8628 wire value is unchanged. Closes WTA-1062
Contributor
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-06-29 04:46:58.776428699 +0000
+++ /tmp/pyrefly_pr.txt 2026-06-29 04:46:43.727343826 +0000
@@ -531,13 +531,23 @@
ERROR `not in` is not supported between `Literal['sessions']` and `None` [not-iterable]
--> tests/integration_tests/controllers/openapi/test_apps.py:210:12
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/integration_tests/controllers/openapi/test_auth.py:29:12
+ --> tests/integration_tests/controllers/openapi/test_auth.py:31:12
+ERROR `None` is not subscriptable [unsupported-operation]
+ --> tests/integration_tests/controllers/openapi/test_auth.py:32:12
+ERROR `None` is not subscriptable [unsupported-operation]
+ --> tests/integration_tests/controllers/openapi/test_auth.py:45:12
+ERROR `None` is not subscriptable [unsupported-operation]
+ --> tests/integration_tests/controllers/openapi/test_auth.py:47:12
+ERROR `None` is not subscriptable [unsupported-operation]
+ --> tests/integration_tests/controllers/openapi/test_auth.py:61:12
+ERROR `None` is not subscriptable [unsupported-operation]
+ --> tests/integration_tests/controllers/openapi/test_auth.py:74:12
ERROR Argument `Literal['normal']` is not assignable to parameter `status` with type `SQLCoreOperations[TenantStatus] | TenantStatus` in function `models.account.Tenant.__init__` [bad-argument-type]
- --> tests/integration_tests/controllers/openapi/test_auth.py:36:52
+ --> tests/integration_tests/controllers/openapi/test_auth.py:81:52
ERROR Object of class `NoneType` has no attribute `get` [missing-attribute]
- --> tests/integration_tests/controllers/openapi/test_auth.py:66:12
+ --> tests/integration_tests/controllers/openapi/test_auth.py:111:12
ERROR Object of class `NoneType` has no attribute `get` [missing-attribute]
- --> tests/integration_tests/controllers/openapi/test_auth.py:91:12
+ --> tests/integration_tests/controllers/openapi/test_auth.py:136:12
ERROR Argument `_GP` is not assignable to parameter `graph_init_params` with type `GraphInitParams` in function `core.workflow.nodes.datasource.datasource_node.DatasourceNode.__init__` [bad-argument-type]
--> tests/integration_tests/core/workflow/nodes/datasource/test_datasource_node_integration.py:85:27
ERROR Argument `_GS` is not assignable to parameter `graph_runtime_state` with type `GraphRuntimeState` in function `core.workflow.nodes.datasource.datasource_node.DatasourceNode.__init__` [bad-argument-type]
@@ -2281,19 +2291,19 @@
ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
--> tests/unit_tests/controllers/openapi/test_device_token.py:31:12
ERROR Object of class `UnprocessableEntity` has no attribute `data` [missing-attribute]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:105:9
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:106:9
ERROR Object of class `UnprocessableEntity` has no attribute `data` [missing-attribute]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:118:16
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:119:16
ERROR Object of class `UnprocessableEntity` has no attribute `data` [missing-attribute]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:122:9
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:123:9
ERROR Object of class `UnprocessableEntity` has no attribute `data` [missing-attribute]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:137:9
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:138:9
ERROR No matching overload found for function `dict.__init__` called with arguments: (dict[str, Any] | None) [no-matching-overload]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:146:20
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:147:20
ERROR Object of class `Conflict` has no attribute `hint` [missing-attribute]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:155:9
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:156:9
ERROR No matching overload found for function `dict.__init__` called with arguments: (dict[str, Any] | None) [no-matching-overload]
- --> tests/unit_tests/controllers/openapi/test_error_contract.py:220:48
+ --> tests/unit_tests/controllers/openapi/test_error_contract.py:221:48
ERROR Object of class `NoneType` has no attribute `total_tokens` [missing-attribute]
--> tests/unit_tests/controllers/openapi/test_models.py:13:12
ERROR Argument `test_verify_approval_grant_raises_on_missing_field._FakeKeyset` is not assignable to parameter `keyset` with type `KeySet` in function `libs.jws.sign` [bad-argument-type]
|
Contributor
Pyrefly Type Coverage
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #38142 +/- ##
==========================================
+ Coverage 85.30% 85.39% +0.09%
==========================================
Files 4931 5119 +188
Lines 256032 262689 +6657
Branches 48544 50082 +1538
==========================================
+ Hits 218396 224331 +5935
- Misses 33389 34066 +677
- Partials 4247 4292 +45
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Distinguishes an expired OAuth bearer token from an unknown/invalid one on the
/openapi/v1auth surface, so clients can tell session expired → re-authenticate apart from never authenticated.Before: both collapsed to a generic
401, and an invalid bearer actually leaked a500.Changes
api/libs/oauth_bearer.py): raises a distinctTokenExpiredErrorfor expired DB rows and records a separateexpirednegative-cache marker, so a retry within the negative-cache TTL still reports expiry instead of degrading to a generic miss.api/controllers/openapi/auth/pipeline.py): maps the two domain errors at the controller boundary to unifiedOpenApiErrorresponses —SessionExpired(codetoken_expired) andInvalidBearer(codeunauthorized), both401. Also fixes the latent500on invalid bearers.token_expiredis synced through the codegen pipeline into the generatedtypes.gen.ts/zod.gen.ts.cli/src/http/error-mapper.ts): branches the401on the generatedtoken_expiredenum. The CLI'sexpired_tokentaxonomy member (RFC 8628 device-flow code expiry) is merged intotoken_expired; the RFC 8628 wire value is unchanged.Tests
token_expiredvsunauthorizedand the invalid→401 (not 500) fix.token_expiredcode.Closes WTA-1062