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
- Fix the client's message order: always Bind the portal (Bind → Execute/Describe) before issuing Describe for it.
- If describing a statement's parameter/row formats is the goal, Describe the prepared statement ('S') instead of the portal ('P') before Bind.
- Update the driver/ORM version if the wrong ordering comes from its extended-protocol implementation.
- 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
- Model the extended-protocol state machine explicitly: Parse → Bind → (Describe|Execute) → Sync.
- Track bound portals client-side; clear tracking when portals are closed.
- Describe 'S' (statement) when you need metadata before binding.
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
- Connection [%s] is not registered.
- BLR is too short: %s
- Unsupported PostgreSQL format code `%s`
- Underlying SQL state: %s, underlying error code: %s.
- Underlying SQL state: %s, underlying error code: %s.
AI-assisted analysis of apache/shardingsphere@e952770a21 (2026-08-14).
Data as JSON: /api/errors/fe3b3aa17797e651.
Report an issue: GitHub.