apache/iceberg · error · NotAuthorizedException

Not authorized

Error message

Not authorized: %s

What it means

RESTClient's default error handler maps HTTP 401 responses to NotAuthorizedException. The server rejected the request because authentication failed or credentials were missing/expired. This is the client-side surface of the server's auth check in the Iceberg REST protocol.

Solutions

  1. Check the catalog properties for authentication (token, credential, oauth2-server-uri) and supply a valid token
  2. Refresh or re-obtain the OAuth2 bearer token; ensure token refresh is enabled
  3. Verify the credential (client-id/secret) is still valid with your auth provider
  4. Inspect the server message for details (e.g. 'invalid token', 'expired credentials')

Example fix

// before
Map<String, String> props = Map.of("uri", "https://catalog.example.com");
// after
Map<String, String> props = Map.of(
    "uri", "https://catalog.example.com",
    "token", validBearerToken);
Defensive patterns

Strategy: try-catch

Validate before calling

if (props.get("token") == null && props.get("credential") == null) {
  throw new IllegalArgumentException("Provide 'token' or 'credential' catalog property for auth");
}

Try / catch

try {
  catalog.loadTable(identifier);
} catch (NotAuthorizedException e) {
  refreshTokenAndRebuildCatalog();
}

Prevention

When it happens

Trigger: HTTP 401 returned by the REST server during any catalog request; e.g. missing Authorization header, expired OAuth2 token, or invalid credentials supplied via AuthConfig/catalog properties.

Common situations: Expired bearer token in a long-running Spark job, missing 'credential' or 'token' catalog property, misconfigured OAuth2 server URL, clock skew invalidating tokens, or rotating secrets without restarting the client.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/ff409e2631b53044. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java:343

    public ErrorResponse parseResponse(int code, String json) {
      try {
        return ErrorResponseParser.fromJson(json);
      } catch (Exception x) {
        LOG.warn("Unable to parse error response", x);
      }
      return ErrorResponse.builder().responseCode(code).withMessage(json).build();
    }

    @Override
    public void accept(ErrorResponse error) {
      switch (error.code()) {
        case 400:
          if (IllegalArgumentException.class.getSimpleName().equals(error.type())) {
            throw new IllegalArgumentException(error.message());
          }
          throw new BadRequestException("Malformed request: %s", error.message());
        case 401:
          throw new NotAuthorizedException("Not authorized: %s", error.message());
        case 403:
          throw new ForbiddenException("Forbidden: %s", error.message());
        case 405:
        case 406:
          break;
        case 500:
          throw new ServiceFailureException("Server error: %s: %s", error.type(), error.message());
        case 501:
          throw new UnsupportedOperationException(error.message());
        case 503:
          throw new ServiceUnavailableException("Service unavailable: %s", error.message());
      }

      throw createRESTException(error);
    }
  }

  private static class OAuthErrorHandler extends ErrorHandler {

View on GitHub (pinned to 86d9c8fc54)