Skip to content

An InterruptException during a TLS read surfaces as ProtocolError, losing the interrupt #247

Closed

Description

@PingoLee

An InterruptException that lands while a task is blocked reading a response arrives bare on a plain-TCP connection. On a TLS connection it surfaces as ProtocolError("TLS transport failure (read): unexpected TLS failure"), and the interrupt is lost. ssl_mode=:preferred is the default, and MySQL 8 offers TLS out of the box, so this is the common case. A caller can't tell a Ctrl-C from a broken TLS record.

Repro

MySQL.jl 2.0.0, Julia 1.12, MySQL 8.4.11:

repro_tls_interrupt.jl
# docker run -d --rm --name mysql84 -p 127.0.0.1:3307:3306 -e MYSQL_ROOT_PASSWORD=pw -e MYSQL_DATABASE=f1 mysql:8.4
# julia -t 2 repro_tls_interrupt.jl
using MySQL, DBInterface
conn(; kw...) = DBInterface.connect(MySQL.Connection, "127.0.0.1", "root", "pw"; db = "f1", port = 3307, kw...)
holder = conn()
DBInterface.execute(holder, "DROP TABLE IF EXISTS driver")
DBInterface.execute(holder, "CREATE TABLE driver (driverid INT PRIMARY KEY, surname VARCHAR(50))")
DBInterface.execute(holder, "INSERT INTO driver VALUES (1, 'Senna')")
for ssl_mode in (:disabled, :preferred)
    victim = conn(; ssl_mode)
    # block `victim` in a read: it waits on a row lock held by `holder`
    DBInterface.execute(holder, "START TRANSACTION")
    DBInterface.execute(holder, "SELECT * FROM driver WHERE driverid = 1 FOR UPDATE")
    t = Threads.@spawn try
        DBInterface.execute(victim, "SELECT * FROM driver WHERE driverid = 1 FOR UPDATE")
        nothing
    catch e
        e
    end
    sleep(0.5)
    schedule(t, InterruptException(); error = true)
    e = fetch(t)
    println("ssl_mode=$ssl_mode: ", typeof(e), " => ", sprint(showerror, e))
    DBInterface.execute(holder, "ROLLBACK")
end
ssl_mode=disabled: InterruptException => InterruptException:
ssl_mode=preferred: MySQL.Protocol.ProtocolError => ProtocolError: TLS transport failure (read): unexpected TLS failure

Cause

fault! receives a Reseau.TLS.TLSError whose cause is the InterruptException. I confirmed this by instrumenting fault! in the repro above. The authenticated-TLS branch then turns it into a ProtocolError, which keeps only err.op and err.message:

err isa Reseau.TLS.TLSError && return ProtocolError("TLS transport failure ($(err.op)): $(err.message)")

The same function already looks through a TLSError for an EOFError cause a few lines up, so the plain-TCP and TLS paths agree on EOF but not on interrupts.

Suggested fix

Unwrap the interrupt the same way as the EOF, before the TLSError → ProtocolError mapping:

err isa Reseau.TLS.TLSError && err.cause isa InterruptException && return err.cause

The session is still marked BROKEN and the transport closed, exactly as on the plain-TCP path. Only the exception the caller sees changes.

Two alternatives, if returning the interrupt is not wanted:

  • keep the TLSError (or its cause) as a cause field on ProtocolError;
  • unwrap any cause generically.

Context

This came out of evaluating MySQL.jl 2.0 as the MySQL backend for PormG, an async-first Julia ORM that already runs on Postgres.jl and SQLite.jl. Its pool treats a bare InterruptException as an abandoned await: it cancels the server-side statement and then recovers or discards the slot. Under TLS that path is never taken. If a PR along these lines would be welcome, I'm glad to write it.

Activity

  1. quinnj commented on Oct 27, 2026

    @quinnj
    Member

    [comment generated via AI]

    Fixed upstream in Reseau.jl 1.6.2 (JuliaServices/Reseau.jl#207): Reseau's TLS read, write, and handshake paths now rethrow an InterruptException instead of wrapping it in a TLSError, so it reaches MySQL.jl unchanged. Your repro against MySQL 8.4 with MySQL.jl 2.0.0 and Reseau 1.6.3 now prints InterruptException for both ssl_mode=:disabled and ssl_mode=:preferred. MySQL.jl's compat already allows these Reseau versions, so Pkg.update() picks it up. Closing; please reopen if you still see it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions