testcontainers/testcontainers-java · error · IllegalArgumentException

Invalid protocol

Error message

Invalid protocol: ${protocol}

What it means

Hidden (the internal r2dbc Testcontainers ConnectionFactory provider) requires a PROTOCOL option of the form '<driverDelegate>:[<protocolDelegate>]' (e.g. 'tc:postgres'). An empty/whitespace-only protocol yields IllegalArgumentException 'Invalid protocol: <protocol>'.

Solutions

  1. Set the protocol to '<driver>:<protocol>' form, e.g. ConnectionFactoryOptions.valueOf("tc:postgresql") or URL r2dbc:tc:postgresql://localhost/d/testdb
  2. Ensure the driver segment before the first ':' is non-empty (e.g. 'tc')
  3. Check that no code path overwrites PROTOCOL with an empty value before create()

Example fix

// before
ConnectionFactoryOptions.builder().option(PROTOCOL, "")...
// after
ConnectionFactoryOptions.builder()
    .option(DRIVER, "tc")
    .option(PROTOCOL, "postgresql")  // resolves to tc:postgresql
    ...
Defensive patterns

Strategy: validation

Validate before calling

String protocol = (String) options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL);
if (protocol == null || protocol.trim().isEmpty() || !protocol.contains(":")) {
    throw new IllegalArgumentException("PROTOCOL must be '<driver>:<db>', e.g. tc:postgresql");
}

Type guard

boolean isValidTcProtocol(Object p) { return p instanceof String s && !s.trim().isEmpty() && s.contains(":") && !s.startsWith(":"); }

Try / catch

try {
    connectionFactory.create();
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Invalid protocol")) { /* rebuild options with tc:<db> */ }
    throw e;
}

Prevention

When it happens

Trigger: Creating a connection factory with ConnectionFactoryOptions.PROTOCOL set to "" or whitespace, or omitting the tc prefix portion so parsing yields nothing usable; called from ConnectionFactory creation flow (create → removeProxying).

Common situations: Building r2dbc URLs manually and forgetting the 'tc:' driver segment (e.g. r2dbc:tc:postgresql:///db vs a malformed protocol); copying options maps and clearing the protocol; dynamic config that returns an empty string.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of testcontainers/testcontainers-java@8e549514e3 (2026-09-12). Data as JSON: /api/errors/e54d7b676501b856. Report an issue: GitHub.

Appendix: source

Thrown at modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java:45

            Object reusable = options.getValue(R2DBCDatabaseContainerProvider.REUSABLE_OPTION);
            if (reusable instanceof String) {
                builder.option(R2DBCDatabaseContainerProvider.REUSABLE_OPTION, Boolean.valueOf((String) reusable));
            }
            return builder.build();
        }

        private ConnectionFactoryOptions removeProxying(ConnectionFactoryOptions options) {
            // To delegate to the next factory provider, inspect the PROTOCOL and convert it to the next DRIVER and PROTOCOL values.
            //
            // example:
            //   | Property | Input           | Output       |
            //   |----------|-----------------|--------------|
            //   | DRIVER   | tc              | postgres     |
            //   | PROTOCOL | postgres        | <empty>      |

            String protocol = (String) options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL);
            if (protocol.trim().length() == 0) {
                throw new IllegalArgumentException("Invalid protocol: " + protocol);
            }
            String[] protocols = protocol.split(":", 2);
            String driverDelegate = protocols[0];

            // when protocol does NOT contain COLON, the length becomes 1
            String protocolDelegate = protocols.length == 2 ? protocols[1] : "";

            return ConnectionFactoryOptions
                .builder()
                .from(options)
                .option(ConnectionFactoryOptions.DRIVER, driverDelegate)
                .option(ConnectionFactoryOptions.PROTOCOL, protocolDelegate)
                .build();
        }

        @Override
        public boolean supports(ConnectionFactoryOptions options) {
            return DRIVER.equals(options.getValue(ConnectionFactoryOptions.DRIVER));

View on GitHub (pinned to 8e549514e3)