apache/iceberg · error · RESTException

Invalid (NOT_MODIFIED) response for request: method=

Error message

Invalid (NOT_MODIFIED) response for request: method=%s, path=%s

What it means

handleResponse rejects an HTTP 304 (NOT_MODIFIED) response when the original request did not send an If-None-Match header. 304 is only meaningful for freshness-aware (ETag/If-None-Match based) loading; receiving one without having sent an ETag means the server is misbehaving or the response is misrouted. The client throws RESTException instead of returning an empty body.

Solutions

  1. Fix or bypass the caching layer (proxy/CDN) that is returning 304 without honoring If-None-Match.
  2. Report/patch the catalog server: it must only return 304 when the request carried If-None-Match.
  3. If the client intended freshness-aware loading, ensure the ETag from the previous response is sent via the If-None-Match request header.
  4. Bypass the proxy for the catalog endpoint to confirm the error disappears without it.

Example fix

// before: server returns 304 regardless of request headers
// after: server-side guard
if (request.getHeader("If-None-Match") != null && etagMatches(request, currentETag)) {
  return Response.notModified().tag(currentETag).build();
}
return Response.ok(payload).build();
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling: verify conditional headers are set when freshness-aware
Preconditions.checkArgument(
    !expecting304 || request.headers().contains("If-None-Match"),
    "304-capable requests must send If-None-Match");

Try / catch

try {
  return client.head(...);
} catch (RESTException e) {
  if (e.getMessage().contains("NOT_MODIFIED")) {
    // server/proxy sends 304 without negotiation — bypass cache or treat as cache bug
  }
  throw e;
}

Prevention

When it happens

Trigger: A request whose responseType permits an empty body (e.g. loadTable with HEAD-style freshness checks) receives status 304 while request.headers() lacks HttpHeaders.IF_NONE_MATCH — i.e. the server sends 304 unconditionally or a proxy/cache injects it.

Common situations: A caching proxy or CDN returning stale 304s; a custom/buggy REST catalog server that always returns 304 for conditional-style endpoints; server upgraded with freshness-aware support but returning 304 even when no ETag was negotiated.

Related errors


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

Appendix: source

Thrown at core/src/main/java/org/apache/iceberg/rest/HTTPClient.java:354

      Consumer<ErrorResponse> errorHandler,
      Consumer<Map<String, String>> responseHeaders,
      ParserContext parserContext)
      throws IOException {
    Map<String, String> respHeaders = Maps.newHashMap();
    for (Header header : response.getHeaders()) {
      respHeaders.put(header.getName(), header.getValue());
    }

    responseHeaders.accept(respHeaders);

    // Skip parsing the response stream for any successful request not expecting a response body
    if (emptyBody(response, responseType)) {
      if (response.getCode() == HttpStatus.SC_NOT_MODIFIED
          && !request.headers().contains(HttpHeaders.IF_NONE_MATCH)) {
        // 304-NOT_MODIFIED is used for freshness-aware loading and requires an ETag sent to the
        // server via IF_NONE_MATCH header in the request. If no ETag was sent, we shouldn't
        // receive a 304.
        throw new RESTException(
            "Invalid (NOT_MODIFIED) response for request: method=%s, path=%s",
            request.method(), request.path());
      }

      return null;
    }

    if (!isSuccessful(response)) {
      // The provided error handler is expected to throw, but a RESTException is thrown if not.
      String responseBody = extractResponseBodyAsString(response);
      throwFailure(response, responseBody, errorHandler);
    }

    if (response.getEntity() == null) {
      throw new RESTException(
          "Invalid (null) response body for request (expected %s): method=%s, path=%s, status=%d",
          responseType.getSimpleName(), request.method(), request.path(), response.getCode());
    }

View on GitHub (pinned to 86d9c8fc54)