grpc/grpc-java · error · IllegalStateException

The transport factory is closed.

Error message

The transport factory is closed.

What it means

CronetChannelBuilder's internal transport factory is closed when the builder's built channel is shut down; after that, any attempt to create a new client transport through the same factory throws IllegalStateException. This is a lifecycle guard preventing transport creation on a torn-down builder/channel.

Source

Thrown at cronet/src/main/java/io/grpc/cronet/CronetChannelBuilder.java:276

        boolean useGetForSafeMethods,
        boolean usePutForIdempotentMethods) {
      usingSharedScheduler = timeoutService == null;
      this.timeoutService = usingSharedScheduler
          ? SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE) : timeoutService;
      this.maxMessageSize = maxMessageSize;
      this.alwaysUsePut = alwaysUsePut;
      this.streamFactory = streamFactory;
      this.executor = Preconditions.checkNotNull(executor, "executor");
      this.transportTracer = Preconditions.checkNotNull(transportTracer, "transportTracer");
      this.useGetForSafeMethods = useGetForSafeMethods;
      this.usePutForIdempotentMethods = usePutForIdempotentMethods;
    }

    @Override
    public ConnectionClientTransport newClientTransport(
        SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) {
      if (closed) {
        throw new IllegalStateException("The transport factory is closed.");
      }
      InetSocketAddress inetSocketAddr = (InetSocketAddress) addr;
      return new CronetClientTransport(streamFactory, inetSocketAddr, options.getAuthority(),
          options.getUserAgent(), options.getEagAttributes(), executor, maxMessageSize,
          alwaysUsePut, transportTracer, useGetForSafeMethods, usePutForIdempotentMethods);
    }

    @Override
    public ScheduledExecutorService getScheduledExecutorService() {
      return timeoutService;
    }

    @Override
    public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials channelCreds) {
      return null;
    }

    @Override

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Create a new channel via CronetChannelBuilder.forAddress(...) instead of reusing the closed one.
  2. Ensure all RPCs are issued before calling channel shutdown, or keep the channel open as long as it is used.
  3. Guard concurrent shutdown vs. new-call creation with application-level synchronization or an atomic 'closed' check.

Example fix

// before
channel.shutdown();
stub.someRpc(request); // IllegalStateException
// after
stub.someRpc(request);
channel.shutdown();
Defensive patterns

Strategy: validation

Validate before calling

if (channel.isShutdown() || channel.isTerminated()) { channel = CronetChannelBuilder.forAddress(host, port, engine).build(); }

Try / catch

try { stub.call(req); } catch (IllegalStateException e) { if (e.getMessage().contains("transport factory is closed")) { channel = rebuildChannel(); } else { throw e; } }

Prevention

When it happens

Trigger: Calling newClientTransport (e.g. by opening a new RPC via the channel's transport) after the CronetChannelBuilder's transport factory has been closed — typically after channel shutdown.

Common situations: Reusing a channel (or its builder) after shutdown() to issue more RPCs; a race where shutdown happens while a new call is being created.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/99a72674ed74f149. Report an issue: GitHub.