apache/iceberg · error · ForbiddenException

Forbidden

Error message

Forbidden: %s

What it means

RESTClient's default error handler maps HTTP 403 responses to ForbiddenException. Authentication succeeded but the server refused to authorize the operation for the given principal — the user lacks permission on the resource.

Solutions

  1. Inspect the server message to identify the denied resource and required privilege
  2. Grant the authenticated principal the required permission on the namespace/table via your catalog's authorization system
  3. Verify you are connecting with the intended credentials/role, not a shared or default account
  4. If the operation should be allowed, check for policy misconfiguration (e.g. wrong namespace ownership)

Example fix

// before
// user has only READ on namespace 'prod'
catalog.loadTable("prod.events").refresh(); // ok
catalog.dropTable("prod.events"); // Forbidden: 403
// after
// run drop with a principal granted TABLE_DROP on prod.events, or
// request the privilege: GRANT DROP ON TABLE prod.events TO ROLE etl;
Defensive patterns

Strategy: try-catch

Try / catch

try {
  catalog.dropTable(identifier);
} catch (ForbiddenException e) {
  log.warn("Permission denied for {}: {}", principal, e.getMessage());
}

Prevention

When it happens

Trigger: HTTP 403 returned by the REST server for any catalog operation (create/drop/rename table, namespace operations) where the authenticated identity lacks the required privilege.

Common situations: Service account without write access attempting commits, namespace-level ACLs blocking table creation, IAM/policy changes removing grants, or attempting admin-only operations with a read-only role.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

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

        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 {
    private static final ErrorHandler INSTANCE = new OAuthErrorHandler();

View on GitHub (pinned to 86d9c8fc54)