Skip to content

Using Response Headers and Body Signals

Source: en/oauth/response-headers.md · Live: https://docs.1pass.dev/en/oauth/response-headers LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).

📍 Jump-to Index

  • L29-L62: ## Signal catalog
    • L31-L41: ### Body fields (4xx responses)
    • L42-L51: ### Response headers (OAuth responses)
    • L52-L62: ### Rate-limit signals (429 responses)
  • L63-L133: ## Pattern 1: Auto-log every 4xx response
    • L65-L90: ### Node.js / Express + axios
    • L91-L111: ### Rails + Faraday
    • L112-L133: ### Python + requests
  • L134-L157: ## Pattern 2: Auto-link traces in Sentry / Datadog
  • L158-L174: ## Pattern 3: Scope drift alerts (200 responses)
  • L175-L193: ## Pattern 4: A one-line message to show the user
  • L194-L201: ## Operations checklist
  • L202-L205: ## Security note
  • L206-L211: ## References

Using Response Headers and Body Signals

In addition to the standard RFC 6749 response, logi attaches signals that your RP server can use to debug and monitor without ever opening the console. This page shows you how to capture those signals at the middleware layer and stream them automatically into your server logs and observability systems.

Signal catalog

Body fields (4xx responses)

FieldAlways presentPurpose
errorMachine-readable code (RFC 6749 §5.2)
error_descriptionOne-line description
error_uriDocs anchor for this code
request_idPer responseThe same key as the console's request_logs

The fields above describe OAuth/RFC error response bodies. A rack-attack throttled 429 uses a separate shape: the body contains only {"error":"rate_limited"} and does not include error_description, error_uri, or request_id. See Rate-limit signals.

Response headers (OAuth responses)

The X-Logi-* headers below are attached to OAuth/OIDC endpoint responses when relevant. A rack-attack throttled 429 response is separate and does not include X-Logi-* headers. See Rate-limit signals.

HeaderWhenPurpose
X-Logi-Request-IdAlways on OAuth/RFC 4xx error responses (except rack-attack 429)Trace even when the body is empty (HEAD, or a failure rendering the error)
X-Logi-Console-UrlOn OAuth/RFC 4xx errors when the RP authenticates successfullyA direct link to this request in the console's request_logs
X-Logi-Scope-DriftOn a token 200 response when drift was recorded within the last 7 daysDetects an unregistered scope — for immediate alerting. Under the default block policy, a drift request is itself rejected with invalid_scope; this header applies to apps explicitly set to log_only/alert with ongoing drift, or as an echo of rejected/recorded drift history

Rate-limit signals (429 responses)

When a request exceeds a rate limit, logi returns HTTP 429 Too Many Requests with:

SignalLocationPurpose
{"error":"rate_limited"}bodyMachine-readable code. The 4xx logging pattern below still captures it
Retry-Afterresponse headerSeconds to wait before retrying. Included on some endpoints and should drive backoff

Rack-attack throttled 429 responses include only those two signals (JSON body + Retry-After). This path does not attach extra X-Logi-* headers, so branch on status 429 + error: "rate_limited". On 429, use Retry-After when present; otherwise apply exponential backoff (1 → 2 → 4 → 8 seconds). Endpoint-specific limits and backoff guidance are in Rate Limits.

Pattern 1: Auto-log every 4xx response

Node.js / Express + axios

js
import axios from 'axios'

const logiClient = axios.create({ baseURL: 'https://api.1pass.dev' })

logiClient.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err.response?.status >= 400 && err.response?.status < 500) {
      const { data, headers, status } = err.response
      console.warn('[logi] 4xx', {
        status,
        error: data?.error,
        error_description: data?.error_description,
        error_uri: data?.error_uri,
        request_id: data?.request_id || headers['x-logi-request-id'],
        console_url: headers['x-logi-console-url'], // an operator can click it right away
      })
    }
    return Promise.reject(err)
  }
)

Rails + Faraday

ruby
class LogiResponseLogger < Faraday::Middleware
  def on_complete(env)
    return unless env.status >= 400 && env.status < 500

    body = JSON.parse(env.body) rescue {}
    Rails.logger.warn(
      "[logi] 4xx",
      status: env.status,
      error: body["error"],
      error_description: body["error_description"],
      error_uri: body["error_uri"],
      request_id: body["request_id"] || env.response_headers["x-logi-request-id"],
      console_url: env.response_headers["x-logi-console-url"]
    )
  end
end

Python + requests

python
import logging, requests

def log_logi_4xx(resp: requests.Response):
    if not (400 <= resp.status_code < 500):
        return
    body = resp.json() if "json" in resp.headers.get("content-type", "") else {}
    logging.warning(
        "[logi] 4xx",
        extra={
            "status": resp.status_code,
            "error": body.get("error"),
            "error_description": body.get("error_description"),
            "error_uri": body.get("error_uri"),
            "request_id": body.get("request_id") or resp.headers.get("X-Logi-Request-Id"),
            "console_url": resp.headers.get("X-Logi-Console-Url"),
        },
    )

If you tie request_id to your RP's distributed tracing key, you can follow a single trace in your own dashboard all the way from "this user's payment failure → logi token issuance failure."

js
import * as Sentry from '@sentry/node'

logiClient.interceptors.response.use(undefined, (err) => {
  if (err.response?.status >= 400) {
    const reqId = err.response.data?.request_id ||
                  err.response.headers['x-logi-request-id']
    Sentry.withScope((scope) => {
      scope.setTag('logi.request_id', reqId)
      scope.setTag('logi.error', err.response.data?.error)
      scope.setExtra('logi.console_url', err.response.headers['x-logi-console-url'])
      Sentry.captureException(err)
    })
  }
  return Promise.reject(err)
})

With this in place, you can jump from a Sentry issue page to the logi console in one click.

Pattern 3: Scope drift alerts (200 responses)

The default scope drift policy is block — a request containing an unregistered scope is rejected with invalid_scope (Scope drift policy). The X-Logi-Scope-Drift header is attached to successful token responses in two cases: when the app is explicitly set to the log_only/alert policy and drift is ongoing (dropped, then proceeding), or as an echo of the drift history recorded within the last 7 days (including block rejections) — the missing scopes arrive as a comma-separated list.

js
logiClient.interceptors.response.use((res) => {
  const drift = res.headers['x-logi-scope-drift']
  if (drift) {
    console.warn('[logi] scope drift detected:', drift.split(','),
                 '— update the registered scopes in the console.')
  }
  return res
})

For the 7 days the drift history stays alive, this signal is attached to every token response, so even if you forget to update a scope before deploying, an alert appears automatically in your server logs.

Pattern 4: A one-line message to show the user

error_description can mix Korean and English and is technical. When showing it to users, we recommend that you maintain a mapping table from the error code to a user-facing message in your RP:

ts
const USER_FACING: Record<string, string> = {
  invalid_grant: 'Your login expired. Please try again.',
  access_denied: 'Login was cancelled.',
  rate_limited: 'Too many requests. Please try again in a moment.',
  invalid_scope: "We can't log you in right now because of an app configuration issue.",
}

function userFacingMessage(error: string) {
  return USER_FACING[error] ?? 'Please try again in a moment.'
}

Send the technical information (error, request_id, console_url) to your server logs and show the user only a friendly one-liner — separating the two this way keeps users from ever seeing something like a stack trace.

Operations checklist

  • [ ] On a 4xx, the RP server records all three of error, error_description, and request_id at the INFO level
  • [ ] The X-Logi-Console-Url header is included in alerting messages (a one-click jump for the operator)
  • [ ] When the X-Logi-Scope-Drift header appears, the RP's monitoring system fires a medium-priority alert
  • [ ] request_id is mapped to the same field as the RP's trace ID, or at minimum printed on the same line
  • [ ] User-facing messages are limited to text that has passed through the error code mapping table

Security note

The extra signals in the response headers and body are not PII — they contain only the request_id, error code, and console URL. They never include user identifiers (email, sub), token values, or secrets, so they are safe to leave in ordinary logs.

References

최종 수정:

Identity가 제품의 신뢰를 만듭니다.