{"record":{"id":"a8ba74ee8dab840d","repo":"apache/iceberg","slug":"cannot-acquire-closed-http-client-clientkey","errorCode":null,"errorMessage":"Cannot acquire closed HTTP client: ${clientKey}","messagePattern":"Cannot acquire closed HTTP client: (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"aws/src/main/java/org/apache/iceberg/aws/HttpClientCache.java","lineNumber":123,"sourceCode":"    private final String clientKey;\n    private volatile int refCount = 0;\n    private boolean closed = false;\n\n    ManagedHttpClient(SdkHttpClient httpClient, String clientKey) {\n      this.httpClient = httpClient;\n      this.clientKey = clientKey;\n      LOG.debug(\"Created managed HTTP client: key={}\", clientKey);\n    }\n\n    /**\n     * Acquire a reference to the HTTP client, incrementing the reference count.\n     *\n     * @return the ref-counted wrapper client\n     * @throws IllegalStateException if the client has already been closed\n     */\n    synchronized ManagedHttpClient acquire() {\n      if (closed) {\n        throw new IllegalStateException(\"Cannot acquire closed HTTP client: \" + clientKey);\n      }\n      refCount++;\n      LOG.debug(\"Acquired HTTP client: key={}, refCount={}\", clientKey, refCount);\n      return this;\n    }\n\n    /**\n     * Release a reference to the HTTP client, decrementing the reference count. If the count\n     * reaches zero, the client is closed.\n     *\n     * @return true if the client was closed, false otherwise\n     */\n    synchronized boolean release() {\n      if (closed) {\n        LOG.warn(\"Attempted to release already closed HTTP client: key={}\", clientKey);\n        return false;\n      }\n","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/aws/src/main/java/org/apache/iceberg/aws/HttpClientCache.java#L105-L141","documentation":"HttpClientCache ref-counts shared AWS SDK HTTP clients. acquire() increments the reference count and hands out the wrapper, but if the underlying client has already been fully released (closed), it throws this IllegalStateException rather than handing out a reference to a closed resource.","triggerScenarios":"Calling acquire() on a cache entry after all previous holders released it and it was closed — e.g. using an S3FileIO/Catalog whose HTTP client was shut down via release/CloseableGroup close, then attempting further requests; test helpers client1/client2 and acquireAfterCloseThrows exercise exactly this.","commonSituations":"Reusing an S3FileIO instance after calling close(); sharing one cached client across threads where one thread closes it while another still needs it; keeping a table/catalog reference alive past its owner's shutdown.","solutions":["Do not use the S3FileIO/catalog after closing it; create a new instance for subsequent operations.","Ensure every user of the shared client calls acquire before use and release when done, so refCount prevents premature close.","Fix thread-lifecycle races so close() happens only after all users are finished (e.g. CloseableGroup managed by a single owner)."],"exampleFix":"// before\nfileIO.close();\nTable table = catalog.loadTable(\"db.t\"); // uses closed HTTP client\n// after\nTable table = catalog.loadTable(\"db.t\");\nfileIO.close(); // close only after all IO is done","handlingStrategy":"try-catch","validationCode":"if (fileIO instanceof Closeable && alreadyClosed) {\n  throw new IllegalStateException(\"S3FileIO was closed; create a new instance\");\n}","typeGuard":"static boolean usable(S3FileIO io) {\n  try { io.properties(); return true; } catch (IllegalStateException e) { return false; }\n}","tryCatchPattern":"try {\n  table.io().newInputFile(location);\n} catch (IllegalStateException e) {\n  if (e.getMessage().contains(\"Cannot acquire closed HTTP client\")) {\n    LOG.error(\"IO was closed before use; recreate it\");\n  }\n  throw e;\n}","preventionTips":["Treat S3FileIO/catalog as single-use: close only at end of lifetime, never mid-use.","Use try-with-resources and keep all reads/writes inside the try block.","Avoid sharing a closed-prone client across threads without a clear ownership owner calling close()."],"tags":["aws","http-client","resource-lifecycle","use-after-close"],"backgroundTag":"invalid-state-transition","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}