apache/shardingsphere · error · InvalidBatchHandleException

Invalid batch handle: statement handle %d

Error message

Invalid batch handle: statement handle %d

What it means

IllegalStateException thrown by Portal.describe() in the PostgreSQL extended-protocol path when a Describe ('P') message arrives before the portal has a bound response header. A portal only gets its QueryResponseHeader or UpdateResponseHeader during Bind; describing before that leaves the header null, and the switch on instanceof falls through to the exception naming the portal.

Source

Thrown at database/protocol/dialect/firebird/src/main/java/org/apache/shardingsphere/database/protocol/firebird/packet/command/query/batch/FirebirdBatchMessageCommandPacket.java:96

     */
    public static int getLength(final FirebirdPacketPayload payload, final List<FirebirdBatchColumnDescriptor> columnDescriptors) {
        int startReaderIndex = payload.getByteBuf().readerIndex();
        if (payload.getByteBuf().readableBytes() < FIXED_BATCH_MSG_HEADER_LENGTH) {
            return -1;
        }
        payload.skipReserved(8);
        return parseBatchMessages(payload, startReaderIndex, payload.readInt4Unsigned(), columnDescriptors);
    }
    
    private static int getLength(final FirebirdPacketPayload payload, final int connectionId, final int startReaderIndex, final int availableBytes) {
        if (availableBytes < FIXED_BATCH_MSG_HEADER_LENGTH) {
            return -1;
        }
        payload.skipReserved(4);
        int statementHandle = payload.readInt4();
        FirebirdBatchStatement batchStatement = FirebirdBatchRegistry.getInstance().getBatchStatement(connectionId, statementHandle);
        if (null == batchStatement) {
            throw new InvalidBatchHandleException(statementHandle);
        }
        return parseBatchMessages(payload, startReaderIndex, payload.readInt4Unsigned(), batchStatement.getColumnDescriptors());
    }
    
    private static int parseBatchMessages(final FirebirdPacketPayload payload, final int startReaderIndex, final long messageCount,
                                          final List<FirebirdBatchColumnDescriptor> columnDescriptors) {
        for (long each = 0; each < messageCount; each++) {
            int messageStartIndex = payload.getByteBuf().readerIndex();
            try {
                parseSingleMessage(payload, columnDescriptors);
                payload.skipPadding(payload.getByteBuf().readerIndex() - messageStartIndex);
                // CHECKSTYLE:OFF
            } catch (final IndexOutOfBoundsException ex) {
                // CHECKSTYLE:ON
                payload.getByteBuf().readerIndex(messageStartIndex);
                return -1;
            }
        }

View on GitHub (pinned to e952770a21)

Solutions

  1. Fix the client's message order: always Bind the portal (Bind → Execute/Describe) before issuing Describe for it.
  2. If describing a statement's parameter/row formats is the goal, Describe the prepared statement ('S') instead of the portal ('P') before Bind.
  3. Update the driver/ORM version if the wrong ordering comes from its extended-protocol implementation.
  4. Add a protocol trace (e.g. psqlodbc log, pgjdbc logger, or Wireshark on Postgres protocol) to confirm Describe precedes Bind, then correct the sequencing.

Example fix

-- before (extended protocol, wrong order)
Parse(stmt='', sql)
Describe(portal='')   -- no Bind yet -> IllegalStateException

-- after
Parse(stmt='', sql)
Bind(stmt='', portal='')
Describe(portal='')
Defensive patterns

Strategy: validation

Validate before calling

// Custom extended-protocol client: enforce Bind-before-Describe per portal
if (!boundPortals.contains(portalName)) {
    throw new IllegalStateException("Portal '" + portalName + "' must be bound before Describe");
}
sendDescribe('P', portalName);

Try / catch

// IllegalStateException arrives as a proxy error response; recover by rebinding
try {
    portal.describe();
} catch (IllegalStateException e) {
    if (e.getMessage().contains("before bind")) {
        bindPortal(portalName, statementName, params); // re-Bind, then retry Describe
        return portal.describe();
    }
    throw e;
}

Prevention

When it happens

Trigger: Sending PostgreSQL extended-protocol message sequence Describe(portal) before Bind for that portal, e.g. Parse; Describe; Sync; or re-describing a portal after it was closed and re-created by Parse without a fresh Bind.

Common situations: Hand-rolled extended-protocol clients or ORMs issuing wrong message ordering; drivers with a bug in pipelining that reorder Describe before Bind; retry logic that replays Parse+Describe but skips Bind; edge behavior after server-side errors that reset portal state.

Related errors


AI-assisted analysis of apache/shardingsphere@e952770a21 (2026-08-14). Data as JSON: /api/errors/fe3b3aa17797e651. Report an issue: GitHub.