Tcl Source Code

View Ticket
Login
Ticket UUID: 0c1e91d072c70f305933d98bc2b24e1c5bf8e48a
Title: Problem with ::http::OpenSocket in HTTP module (library/http/http.tcl) since Tcl 9.0.x
Type: Bug Created on: 2026-10-01 12:02:40
Submitter: robgcs Assigned to: jan.nijtmans
Subsystem: 29. http Package Severity: Critical
Priority: 5 Medium Last modified: 2026-10-16 13:08:05
Status: Closed Closed by: jan.nijtmans
Resolution: Fixed Closed on: 2026-10-16 13:08:05
Version: 9.0.5, 9.1.0
Description:
Hello, everybody

We are currently in the process of porting our application from Tcl/Tk 8.6 to 9.0. In the process, we have encountered a problem with zooming in and out within our WMS (Web Map Service) module.

The error occurs under Linux (openSUSE Leap 16.0 and Debian 13) with Tcl 9.0.3 (and most likely with 9.0.4 as well), but not with Tcl 8.6.16.

The source code for Tcl 9.0.3 contains the file `library/http/http.tcl`, and line 1727 reads as follows:
    set sock [namespace eval :: $state(openCmd)]
Here, the command previously registered via `http::register` is executed. If this command, for example, executes a `vwait` (as occurs, for instance, in the `autoproxy` package [autoproxy::tls_socket]), then events are processed in the meantime and their handlers are executed. These handlers can, in turn, call `http::reset` and `http::cleanup`, so that the token (the variable `state` in ::http::OpenSocket) no longer exists after returning from line 1727.

The HTTP module then terminates, for example, with the following error message, which is difficult to catch/handle:
    can't read "state(-keepalive)": no such element in array

I have attached a small script that can be used to reproduce the error, and also a quick-and-dirty patch (see below).

Kind regards,

Robert

#!/usr/bin/env wish
package require http
package require tls

wm title . "Logo"
wm minsize . 300 300

set waitForMe 0

proc lameTlsSocket {args} {
   # Let's wait a little bit
   vwait ::waitForMe
   return [tls::socket {*}$args]
}
http::register https 443 lameTlsSocket

label .label -text "Use the keyboard or mouse to generate a series of events" -wraplength 300
pack .label -expand 1

proc getLogo {args} {
   .label configure -image ""

   if {[info exists ::currentRequest]} {
      # Stop current request
      puts "Stop current request $::currentRequest"
      http::reset $::currentRequest
   }

   # Start new request
   set ::currentRequest [http::geturl "https://thumb.wikimedia.org/wikipedia/commons/thumb/4/41/Tcl.svg/120px-Tcl.svg.png" -command displayLogo]
   puts "New request $::currentRequest"
}
bind . <MouseWheel> getLogo
bind . <Button> getLogo
bind . <Key> getLogo

proc displayLogo {token} {
   if {[http::status $token] eq "ok"} {
      set data [http::data $token]
      try {
         image create photo logo -format png -data $data
         .label configure -image logo
      } on error {err} {
         puts $err
      }
   }

   puts "Cleanup $token"
   http::cleanup $token

   if {$token eq $::currentRequest} {
      unset ::currentRequest
   }
}

proc goOn {} {
   incr ::waitForMe
   after 1000 goOn
}
goOn
### END OF SCRIPT

--- library/http/http.tcl
+++ library/http/http.tcl
@@ -1754,7 +1754,7 @@
     dict unset socketCoEvent($state(socketinfo)) $token
     unset -nocomplain state(socketcoro)

-    if {[catch {
+    try {
        if {$state(reusing)} {
            # If ($state(reusing)) is true, then we do not need to create a new
            # socket, even if $sockOld is only a placeholder for a socket.
@@ -1765,6 +1765,10 @@
            ##Log pre socket opened, - token $token
            ##Log $state(openCmd) - token $token
            set sock [namespace eval :: $state(openCmd)]
+       if {![info exists state]} {
+          catch {close $sock}
+          return
+       }
            set state(sock) $sock
            # Normal return from $state(openCmd) always returns a valid socket.
            # A TLS proxy connection with 407 or other failure from the
@@ -1791,7 +1795,7 @@
        # Code above has set state(sock) $sock
        ConfigureNewSocket $token $sockOld $DoLater
        ##Log OpenSocket success $sock - token $token
-    } result errdict]} {
+    } on error {result errdict} {
        ##Log OpenSocket failed $result - token $token
        # There may be other requests in the socketPhQueue.
        # Prepare socketPlayCmd so that Finish will replay them.
User Comments:
oehhar added on 2026-10-01 12:51:30:

Hi Robert,

explanation and patch looks good.

You have replaced catch by try for practical reasons?

Others will probably take care. I don't understand the http package so much...

THanks again, Harald


robgcs added on 2026-10-01 13:27:36:
Hi Harald
Without the try{}, the `return` statement would not have worked as intended (the `return` statement would have caused a jump to the error-handling code).
Kind regards,
Robert

serhiy.storchaka added on 2026-10-14 15:15:32:

Reproduced with 9.0.5 and 9.1 (8.6 opens the socket synchronously in [http::geturl], so it is not affected). Besides the error after [http::cleanup] (which also leaks the new socket), a plain [http::reset] while the socket command runs the event loop is not respected: the request continues and finally gets status "ok".

Proposed fix in branch http-reset-during-open: after the socket command returns, http::OpenSocket checks whether the request has been reset or cleaned up, and if so closes the new socket and returns. Tests http-4.17 and http-4.18.


jan.nijtmans added on 2026-10-16 13:08:05:

Fixed in [2d6eb233b6|trunk] and core-9-0-branch