http(3tcl)
π http(3tcl) π
Tcl Bundled Packages
okay bestie, welcome to the http manual page, no cap. This is the client-side implementation of HTTP/1.1 and it's lowkey iconic. Let's get into it.
β¨ NAME β¨
http β Client-side implementation of the HTTP/1.1 protocol
π¦ SYNOPSIS π¦
package require http ?2.10?
::http::config ?-option value ...?
::http::geturl url ?-option value ...?
::http::formatQuery key value ?key value ...?
::http::quoteString value
::http::reset token ?why?
::http::wait token
::http::status token
::http::size token
::http::error token
::http::postError token
::http::cleanup token
::http::requestLine token
::http::requestHeaders token ?headerName?
::http::requestHeaderValue token headerName
::http::responseLine token
::http::responseCode token
::http::reasonPhrase code
::http::responseHeaders token ?headerName?
::http::responseHeaderValue token headerName
::http::responseInfo token
::http::responseBody token
::http::register proto port command ?socketCmdVarName? ?useSockThread? ?endToEndProxy?
::http::registerError sock ?message?
::http::unregister proto
::http::code token
::http::data token
::http::meta token ?headerName?
::http::metaValue token headerName
::http::ncode token
π EXPORTED COMMANDS π
Namespace http exports the commands config, formatQuery, geturl, postError, quoteString, reasonPhrase, register, registerError, requestHeaders, requestHeaderValue, requestLine, responseBody, responseCode, responseHeaders, responseHeaderValue, responseInfo, responseLine, reset, unregister, and wait.
It does not export the commands cleanup, code, data, error, meta, metaValue, ncode, size, or status.
π«Ά DESCRIPTION π«Ά
okay so the http package gives you the client side of the HTTP/1.1 protocol, as defined in RFC 9110 to 9112, which supersede RFC 7230 to RFC 7235, which in turn supersede RFC 2616. The package implements the GET, POST, and HEAD operations of HTTP/1.1. It lets you configure a proxy host to get through firewalls. It's compatible with the Safesock security policy, so it can be used by untrusted applets to do URL fetching from a restricted set of hosts. This package can be extended to support additional HTTP transport protocols, such as HTTPS, by providing a custom socket command, via ::http::register.
The ::http::geturl procedure does a HTTP transaction. Its options determine whether a GET, POST, or HEAD transaction is performed. The return value of ::http::geturl is a token for the transaction. The token can be supplied as an argument to other commands, to manage the transaction and examine its results.
If the -command option is specified, then the HTTP operation is done in the background. ::http::geturl returns immediately after generating the HTTP request and the -command callback is invoked when the transaction completes. For this to work, the Tcl event loop must be active. In Tk applications this is always true. For pure-Tcl applications, the caller can use ::http::wait after calling ::http::geturl to start the event loop.
Note: The event queue is even used without the-commandoption. As a side effect, arbitrary commands may be processed whilehttp::geturlis running.
When the HTTP server has replied to the request, call the command ::http::responseInfo, which returns a dict of metadata that is essential for identifying a successful transaction and making use of the response. See section METADATA for details of the information returned. The response itself is returned by command ::http::responseBody, unless it has been redirected to a file by the -channel option of ::http::geturl.
π COMMANDS π
::http::config ?options?
The ::http::config command is used to set and query the name of the proxy server and port, and the User-Agent name used in the HTTP requests. If no options are specified, then the current configuration is returned. If a single argument is specified, then it should be one of the flags described below. In this case the current value of that setting is returned. Otherwise, the options should be a set of flags and values that define the configuration:
-accept mimetypesβ The Accept header of the request. The default is*/*, which means that all types of documents are accepted. Otherwise you can supply a comma-separated list of mime type patterns that you are willing to receive. For example,image/gif, image/jpeg, text/*.-cookiejar commandβ The cookie store for the package to use to manage HTTP cookies.commandis a command prefix list; if the empty list (the default value) is used, no cookies will be sent by requests or stored from responses. The command indicated bycommand, if supplied, must obey the COOKIE JAR PROTOCOL described below.-pipeline booleanβ Specifies whether HTTP/1.1 transactions on a persistent socket will be pipelined. See the PERSISTENT SOCKETS section for details. The default is1.-postfresh booleanβ Specifies whether requests that use the POST method will always use a fresh socket, overriding the-keepaliveoption of commandhttp::geturl. See the PERSISTENT SOCKETS section for details. The default is0.-proxyauth stringβ If non-empty, the string is supplied to the proxy server as the value of the request headerProxy-Authorization. This option can be used for HTTP Basic Authentication. If the proxy server requires authentication by another technique, e.g. Digest Authentication, the-proxyauthoption is not useful. In that case the caller must expect a407response from the proxy, compute the authentication value to be supplied, and use the-headersoption to supply it as the value of theProxy-Authorizationheader.-proxyfilter commandβ The command is a callback that is made during::http::geturlto determine if a proxy is required for a given host. One argument, a host name, is added tocommandwhen it is invoked. If a proxy is required, the callback should return a two-element list containing the proxy server and proxy port. Otherwise the filter command should return an empty list.
The default value of -proxyfilter is http::ProxyRequired, and this command returns the values of the -proxyhost and -proxyport settings if they are non-empty. The options -proxyhost, -proxyport, and -proxynot are used only by http::ProxyRequired, and nowhere else in ::http::geturl. A user-supplied -proxyfilter command may use these options, or alternatively it may obtain values from elsewhere in the calling script. In the latter case, any values provided for -proxyhost, -proxyport, and -proxynot are unused.
The ::http::geturl command runs the -proxyfilter callback inside a catch command. Therefore an error in the callback command does not call the bgerror handler. See the ERRORS section for details.
-proxyhost hostnameβ The host name or IP address of the proxy server, if any. If this value is the empty string, the URL host is contacted directly. See-proxyfilterfor how the value is used.-proxynot listβ A Tcl list of domain names and IP addresses that should be accessed directly, not through the proxy server. The target hostname is compared with each list element using a case-insensitive string match. It is often convenient to use the wildcard*at the start of a domain name (e.g.*.example.com) or at the end of an IP address (e.g.192.168.0.*). See-proxyfilterfor how the value is used.-proxyport numberβ The port number of the proxy server. See-proxyfilterfor how the value is used.-repost booleanβ Specifies what to do if a POST request over a persistent connection fails because the server has half-closed the connection. If boolean true, the request will be automatically retried; if boolean false it will not, and the application that useshttp::geturlis expected to seek user confirmation before retrying the POST. The value true should be used only under certain conditions. See the PERSISTENT SOCKETS section for details. The default is0.-threadlevel levelβ Specifies whether and how to use the Thread package. Possible values oflevelare0,1or2.0β (the default) do not use Thread1β use Thread if it is available, do not use it if it is unavailable2β use Thread if it is available, raise an error if it is unavailable
The Tcl socket -async command can block in adverse cases (e.g. a slow DNS lookup). Using the Thread package works around this problem, for both HTTP and HTTPS transactions. Values of level other than 0 are available only to the main interpreter in each thread. See section THREADS for more information.
-urlencoding encodingβ The encoding used for creating the x-url-encoded URLs with::http::formatQueryand::http::quoteString. The default isutf-8, as specified by RFC 2718.-useragent stringβ this sets the value of theUser-Agentheader in the HTTP request, bestie. In an unsafe interpreter, the default value depends on your operating system, plus the version numbers ofhttpandTcl, and itβs literally (for example) βMozilla/5.0 (Windows; U; Windows NT 10.0) http/2.10.2 Tcl/9.0.0β. But a safe interpreter canβt determine its OS, so the default there is to use a Windows 10 value with the current version numbers ofhttpandTcl. π-zip booleanβ if the value is boolean true, then by default requests will send a headerAccept-Encoding: gzip,deflate. If itβs boolean false, then by default theyβll sendAccept-Encoding: identity. Either way, you can override the default for a single request by supplying a customAccept-Encodingheader in the-headersoption ofhttp::geturl. The default value is1. β¨
π ::http::geturl url ?options? π
The ::http::geturl command is the main character of the package, no cap. π If you use -query or -querychannel, it does a POST operation; if you use -validate, it does a HEAD operation; otherwise, itβs a GET. It returns a token value that you can pass as an argument to other commands to get info about the transaction. See the METADATA and ERRORS sections for the tea. βοΈ The ::http::geturl command blocks until the operation completes, unless the -command option specifies a callback that gets invoked when the HTTP transaction completes. Here are all the options it takes, bestie:
-binary booleanβ specifies whether to force interpreting the URL data as binary. Normally this is auto-detected (anything not beginning with a text content type, or whose content encoding isgzipordeflate, is considered binary data). π§-blocksize sizeβ the block size used when reading the URL. At mostsizebytes are read at once. After each block, a call to the-progresscallback is made (if that option is specified). π-channel nameβ copy the URL contents to channelnameinstead of saving it in a Tcl variable for retrieval by::http::responseBody. πΈ-command callbackβ the presence of this option causes::http::geturlto return immediately. After the HTTP transaction completes, the value ofcallbackis expanded, an additional argument is added, and the resulting command is evaluated. The additional argument is the token returned from::http::geturl. This token is the name of an array that is described in the STATE ARRAY section. Hereβs a template for the callback:
proc httpCallback {token} {
upvar 0 $token state
# Access state as a Tcl array defined in this proc
...
return
}
The ::http::geturl command runs the -command callback inside a catch command. Therefore an error in the callback command does not call the bgerror handler. See the ERRORS section for details. π
-guesstype booleanβ attempt to guess theContent-Typeand character set when a misconfigured server provides no information. The default value isfalse(do nothing). If boolean true then, if the server does not send aContent-Typeheader, or if it sends the value"application/octet-stream",http::geturlwill attempt to guess appropriate values. This is not intended to become a general-purpose tool, and currently it is limited to detecting XML documents that begin with an XML declaration. In this case theContent-Typeis changed to"application/xml", the binary flagstate(binary)is changed to0, and the character set is changed to the one specified by the"encoding"tag of the XML line, or toutf-8if no encoding is specified. Not used if a-channelis specified. π¦-handler callbackβ if this option is absent,http::geturlprocesses incoming data itself, either appending it to thestate(body)variable or writing it to the-channel. But if the-handleroption is present,http::geturldoes not do this processing and instead callscallback. Whenever HTTP data is available, the value ofcallbackis expanded, an additional two arguments are added, and the resulting command is evaluated. The two additional arguments are: the socket for the HTTP data and the token returned from::http::geturl. The token is the name of a global array that is described in the STATE ARRAY section. The procedure is expected to return the number of bytes read from the socket. Hereβs a template for the callback:
proc httpHandlerCallback {socket token} {
upvar 0 $token state
# Access socket, and state as a Tcl array defined in this proc
# For example...
...
set data [read $socket 1000]
set nbytes [string length $data]
...
return $nbytes
}
The http::geturl code for the -handler option is not compatible with either compression or chunked transfer-encoding. If -handler is specified, then to work around these issues http::geturl will reduce the HTTP protocol to 1.0, and override the -zip option (i.e. it will send the header Accept-Encoding: identity instead of Accept-Encoding: gzip,deflate). π
If options -handler and -channel are used together, the handler is responsible for copying the data from the HTTP socket to the specified channel. The name of the channel is available to the handler as element -channel of the token array. π
The ::http::geturl command runs the -handler callback inside a catch command. Therefore an error in the callback command does not call the bgerror handler. See the ERRORS section for details. π
-headers keyvaluelistβ this option is used to add headers not already specified by::http::configto the HTTP request. Thekeyvaluelistargument must be a list with an even number of elements that alternate between keys and values. The keys become header field names. Newlines are stripped from the values so the header cannot be corrupted. For example, ifkeyvaluelistisPragma no-cachethen the following header is included in the HTTP request:
Pragma: no-cache
-keepalive booleanβ if boolean true, attempt to keep the connection open for servicing multiple requests. Default is0. π₯-method typeβ force the HTTP request method totype.::http::geturlwill auto-selectGET,POSTorHEADbased on other options, but this option overrides that selection and enables choices likePUTandDELETEfor WebDAV support. π
It is the callerβs responsibility to ensure that the headers and request body (if any) conform to the requirements of the request method. For example, if using -method POST to send a POST with an empty request body, the caller must also supply the option:
-headers {Content-Length 0}
-myaddr addressβ pass a specific local address to the underlying socket call in case multiple interfaces are available. π-progress callbackβ if the-progressoption is present, then thecallbackis made after each transfer of data from the URL. The value ofcallbackis expanded, an additional three arguments are added, and the resulting command is evaluated. The three additional arguments are: the token returned from::http::geturl, the expected total size of the contents from theContent-Lengthresponse header, and the current number of bytes transferred so far. The token is the name of a global array that is described in the STATE ARRAY section. The expected total size may be unknown, in which case zero is passed to the callback. Hereβs a template for the progress callback:
proc httpProgress {token total current} {
upvar 0 $token state
# Access state as a Tcl array defined in this proc
...
return
}
-protocol versionβ select the HTTP protocol version to use. This should be1.0or1.1(the default). Should only be necessary for servers that do not understand or otherwise complain about HTTP/1.1. π-query queryβ this flag (if the value is non-empty) causes::http::geturlto do a POST request that passes the stringqueryverbatim to the server as the request payload. The content format (and encoding) ofqueryis announced by the request headerContent-Typewhich is set by the option-type. Any value of-typeis permitted, and it is the responsibility of the caller to supplyqueryin the correct format. π
(first bit of -type, since it's already half-mentioned above β no shade, just continuity β¨)
-typemime-type β ok so this one sets theContent-Typevalue to mime-type instead of the defaultapplication/x-www-form-urlencodedduring aPOSTop. It's giving "I want to send something that isn't a form, bestie." π-queryblocksizesize β the block size used when posting query data to the URL. At most size bytes get written at once, okay? And after each block, a call to the-queryprogresscallback happens (but only if you actually specified that option, no cap). π-querychannelchannelID β this flag makes::http::geturldo aPOSTrequest that passes the data living inside channelID to the server. That data must be an x-url-encoding formatted query unless you're using the-typeoption. Oh and if you didn't specify aContent-Lengthheader via-headers, then::http::geturltries to figure out the size of the post data itself so it can make that header. If it can't determine the size? It returns an error. Sorry bestie, it tried. π-queryprogresscallback β if-queryprogressis present, the callback gets called after each transfer of data to the URL in aPOSTrequest (i.e. a call to::http::geturlwith option-queryor-querychannel). It acts exactly like the-progressoption β same callback format, literally identical vibe. π«Ά-strictboolean β if true, the command tests that the URL complies with RFC 3986, i.e. that it has no characters that should be "x-url-encoded" (like, a space should be encoded to%20, periodt). Default value is1. β¨-timeoutmilliseconds β if milliseconds is non-zero, then::http::geturlsets up a timeout to occur after that many milliseconds. A timeout results in a call to::http::resetand to the-commandcallback (if you specified one). And after a timeout has occurred, the return value of::http::status(and the value of thestatuskey in the dictionary from::http::responseInfo) istimeout. The drama of it all. π-validateboolean β if boolean is non-zero, then::http::geturldoes an HTTPHEADrequest. The server returns the same status line and response headers as it would for a HTTPGETrequest, but it omits the response entity (the URL "contents"). After the transaction, those response headers are available using::http::responseHeadersor, for selected info,::http::responseInfo. π
π¦ MORE COMMANDS (the deep lore continues) π¦
::http::formatQuery key value ?key value ...?
This procedure does x-url-encoding of query data, and honestly? It eats. It takes an even number of arguments that are the keys and values of the query. It encodes the keys and values, and generates one single string that has the proper & and = separators. The result is suitable for the -query value passed to ::http::geturl. πΈ
::http::quoteString value
This procedure does x-url-encoding of a string. It takes a single argument and encodes it. Simple, iconic, no notes. π
::http::reset token ?why?
This command resets the HTTP transaction identified by token, if any. It sets the state(status) value to why, which defaults to reset, and then calls the registered -command callback. Clean exit energy. π
::http::wait token
This command blocks and waits for the transaction to complete. Heads up though: this only works in trusted code because it uses vwait. Also, it's not useful when ::http::geturl is called without the -command option β because in that case the ::http::geturl call doesn't even return until the HTTP transaction is complete, so there's literally nothing to wait for. Not me waiting for something that already finished. π
::http::status token
This command returns a description of the status of the HTTP transaction. The return value is the empty string until the HTTP transaction is completed; after completion it has one of the values ok, eof, error, timeout, and reset. The meaning of these values is described in the section ERRORS (below). π
The name "status" is not related to the terms "status line" and "status code" that are defined for a HTTP response. Do not get it twisted, bestie.
::http::size token
This command returns the number of bytes received so far from the URL in the ::http::geturl call. Numbers, we love to see them. π
::http::error token
This command returns the error information if the HTTP transaction failed, or the empty string if there was no error. The info is a Tcl list of the error message, stack trace, and error code. π«
::http::postError token
A POST request is a call to ::http::geturl with either the -query or -querychannel option, just so we're clear. The ::http::postError command returns the error information generated when a HTTP POST request sends its request-body to the server; or the empty string if there was no error. The info is a Tcl list of the error message, stack trace, and error code. When this type of error occurs, the ::http::geturl command continues the transaction and attempts to receive a response from the server. It's giving "the show must go on." π₯
::http::cleanup token
This procedure cleans up the state associated with the connection identified by token. After this call, procedures like ::http::responseBody cannot be used to get information about the operation. It is strongly recommended that you call this function after you're done with a given HTTP request. Not doing so will result in memory not being freed, and if your app calls ::http::geturl enough times, the memory leak could cause a performance hit...or worse. π
Bestie. Call ::http::cleanup. This is not a drill. The memory leak is real and it is coming.
::http::requestLine token
This command returns the "request line" sent to the server. The "request line" is the first line of a HTTP client request, and it has three elements separated by spaces: the HTTP method, the URL relative to the server, and the HTTP version. Examples:
GET / HTTP/1.1
GET /introduction.html?subject=plumbing HTTP/1.1
POST /forms/order.html HTTP/1.1
::http::requestHeaders token ?headerName?
This command returns the HTTP request header names and values, in the order that they were sent to the server, as a Tcl list of the form ?name value ...?. Header names are case-insensitive and are converted to lower case. The return value is not a dict because some header names may occur more than once (rude of them, honestly). If one argument is supplied, all request headers are returned. If two arguments are supplied, the second provides the value of a header name. Only headers with the requested name (converted to lower case) are returned. If no such headers are found, an empty list is returned. π
::http::requestHeaderValue token headerName
This command returns the value of the HTTP request header named headerName. Header names are case-insensitive and get converted to lower case. If no such header exists, the return value is the empty string. If there are multiple headers named headerName, the result is obtained by joining the individual values with the string ", " (comma and space), preserving their order. Neat and tidy, periodt. β
::http::responseLine token
This command returns the first line of the server response: the HTTP "status line". The "status line" has three elements separated by spaces: the HTTP version, a three-digit numerical "status code", and a "reason phrase". Only the reason phrase may contain spaces. Examples:
HTTP/1.1 200 OK
HTTP/1.0 404 Not Found
The "status code" is a three-digit number in the range 100 to 599. A value of 200 is the normal return from a GET request, and its matching "reason phrase" is "OK". Codes beginning with 4 or 5 indicate errors. Codes beginning with 3 are redirection errors β in this case the Location response header specifies a new URL that contains the requested information. π
The "reason phrase" is a textual description of the "status code": it may vary from server to server, and can be changed without affecting the HTTP protocol. The recommended values (RFC 7231 and IANA assignments) for each code are provided by the command ::http::reasonPhrase. πΈ
::http::responseCode token
This command returns the "status code" (200, 404, etc.) of the server "status line". If a three-digit code cannot be found, the full status line is returned. See command ::http::responseLine for more info on the "status line". π
::http::reasonPhrase code
This command returns the IANA recommended "reason phrase" for a particular "status code" returned by a HTTP server. The argument code is a valid status code, and therefore is an integer in the range 100 to 599 inclusive. For numbers in this range with no assigned meaning, the command returns the value "Unassigned". Several status codes are used only in response to the methods defined by HTTP extensions such as WebDAV, and not in response to a HEAD, GET, or POST request method. π¦
The "reason phrase" returned by a HTTP server may differ from the recommended value, without affecting the HTTP protocol. The value returned by ::http::geturl can be obtained by calling either command ::http::responseLine (which returns the full status line) or command ::http::responseInfo (which returns a dictionary, with the "reason phrase" stored in key reasonPhrase). π
A registry of valid status codes is maintained at https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml β go peek, iykyk. π
(still more to come, bestie β don't go anywhere β¨)
::http::responseHeaders token ?headerName?β bestie, when a server replies it doesn't just hand over the body, it also ships β¨metadata headersβ¨ that describe the body and the transaction itself. This command hands you back the response header names and values, in the exact order they came off the wire from the server, as a Tcl list shaped like?name value ...?. Header names are case-insensitive and get converted to lower case. π
ok important: the return value is not a dict, because some header names can show up more than once β Set-Cookie being the icon of doing this. π
If you don't pass the second argument, you get all the response headers. If you do pass it, it provides the value of a header name, and you only get headers with the requested name (converted to lower case). If there are no such headers, you get an empty list back. Peep the METADATA section for more, iykyk.
::http::responseHeaderValue token headerNameβ this one returns the value of the HTTP response header named headerName. Header names are case-insensitive and get converted to lower case. If no such header exists, the return value is the empty string, no drama. π«Ά
If there are multiple headers named headerName, the result is obtained by joining the individual values together with the string ", " (comma and space), and their order is preserved. Multiple headers with the same name may be processed this way β except Set-Cookie, which does not conform to the comma-separated-list syntax and cannot be combined into a single value. Each Set-Cookie header must be treated individually, e.g. by processing the return value of ::http::responseHeaders token Set-Cookie. No merging those, periodt. πͺ
::http::responseInfo tokenβ returns a dict of selected response metadata that are essential for identifying a successful transaction and making use of the response, plus other metadata that's just informational. The keys of the dict arestage,status,responseCode,reasonPhrase,contentType,binary,redirection,upgrade,error,postError,method,charset,compression,httpRequest,httpResponse,url,connectionRequest,connectionResponse,connectionActual,transferEncoding,totalPost,currentPost,totalSize, andcurrentSize. What all of those mean? It's all spelled out in the METADATA section below. π
It is always worth checking the value of binary after a HTTP transaction, to determine whether a misconfigured server has caused http to interpret a text resource as a binary, or vice versa. Vibe check your binary, fr fr. π
After a POST transaction, check the value of postError to verify that the request body was uploaded without error. Do not skip this one, bestie, I'm begging. π
::http::responseBody tokenβ returns the entity sent by the HTTP server (unless-channelwas used, in which case the entity was delivered to the channel, and the command returns the empty string). That's the β¨main characterβ¨ of your request right there.
Other terms for "entity", with varying precision, include "representation of resource", "resource", "response body after decoding", "payload", "message body after decoding", "content(s)", and "file". Same girl, different names. π
::http::register proto port command ?socketCmdVarName? ?useSockThread? ?endToEndProxy?β this procedure lets you provide custom HTTP transport types such as HTTPS, by registering a prefix, the default port, and the command to execute to create the Tcl channel. The optional arguments configure how http uses the custom transport, and they have default values that are compatible with older versions of http in which::http::registerhas no optional arguments. Glow-up, but backwards compatible. β¨
Argument socketCmdVarName is the name of a variable provided by the transport, whose value is the command used by the transport to open a socket. Its default value is set by the transport and is "::socket", but if the name of the variable is supplied to ::http::register, then http will set a new value in order to make optional facilities available. Those facilities are enabled by the optional arguments useSockThread and endToEndProxy, which take boolean values with default value false.
Iff argument useSockThread is supplied and is boolean true, then iff permitted by the value [http::config -threadlevel] and by the availability of package Thread, sockets created for the transport will be opened in a different thread so that a slow DNS lookup will not cause the script to block. Multithreading said "not me fixing your blocking problem". π§
Iff argument endToEndProxy is supplied and is boolean true, then when http::geturl accesses a server via a proxy, it will open a channel by sending a CONNECT request to the proxy, and it will then make its request over this channel. This allows end-to-end encryption for HTTPS requests made through a proxy. π
For example,
package require http
package require tls
::http::register https 443 ::tls::socket ::tls::socketCmd 1 1
set token [::http::geturl https://my.secure.site/]
::http::registerError sock ?message?β this procedure allows a registered protocol handler to deliver an error message for use by http. Lowkey important: calling this command does not raise an error. It's useful when a registered protocol detects a problem (for example, an invalid TLS certificate) that will cause an error to propagate to http. The command allows http to provide a precise error message rather than a general one. No vague "something went wrong" energy here. π
The command returns the value provided by the last call with argument message, or the empty string if no such call has been made.
::http::unregister protoβ this procedure unregisters a protocol handler that was previously registered via::http::register, returning a six-item list of the values that were previously supplied to::http::registerif there was such a handler, and an error if there was no such handler. Slay, you get your receipts back. π§Ύ::http::code tokenβ an alternative name for the command::http::responseLine::http::data tokenβ an alternative name for the command::http::responseBody.::http::meta token ?headerName?β an alternative name for the command::http::responseHeaders::http::ncode tokenβ an alternative name for the command::http::responseCode
π ERRORS π
Okay so here's where things can go sideways, and we are keeping it real. The ::http::geturl procedure will raise errors in the following cases: invalid command line options, or an invalid URL. These errors mean that it cannot even start the network transaction. For synchronous ::http::geturl calls (where -command is not specified), it will raise an error if the URL is on a non-existent host or at a bad port on an existing host. It will also raise an error for any I/O errors while writing out the HTTP request line and headers, or reading the HTTP reply headers or data. Because ::http::geturl does not return a token in these cases, it does all the required cleanup and there is no issue of your app having to call ::http::cleanup. π
For asynchronous ::http::geturl calls, all of the above error situations apply, except that if there is any error while reading the HTTP reply headers or data, no exception is thrown. This is because after writing the HTTP headers, ::http::geturl returns, and the rest of the HTTP transaction occurs in the background. The command callback can check if any error occurred during the read by calling ::http::responseInfo to check the transaction status. π
Alternatively, if the main program flow reaches a point where it needs to know the result of the asynchronous HTTP request, it can call ::http::wait and then check status and error, just as the synchronous call does.
The ::http::geturl command runs the -command, -handler, and -proxyfilter callbacks inside a catch command. Therefore an error in the callback command does not call the bgerror handler. When debugging one of these callbacks, it may be convenient to report errors by using a catch command within the callback command itself, e.g. to write an error message to stdout.
In any case, you must still call ::http::cleanup to delete the state array when you are done. Do not forget, bestie, this is the one that gets people. π€
There are other possible results of the HTTP transaction determined by examining the status from ::http::status (or the value of the status key in the dictionary returned by ::http::responseInfo). These are described below:
okβ if the HTTP transaction completes entirely, then status will beok. However, you should still check the::http::responseLinevalue to get the HTTP status. The::http::responseCodeprocedure provides just the numeric error (e.g.,200,404or500) while the::http::responseLineprocedure returns a value like βHTTP 404 File not foundβ.eofβ if the server closes the socket without replying, then no error is raised, but the status of the transaction will beeof.errorβ the error message, stack trace, and error code are accessible via::http::error. The error message is also provided by the value of theerrorkey in the dictionary returned by::http::responseInfo.timeoutβ a timeout occurred before the transaction could complete. β°resetβ the user has called::http::reset.""β (empty string) the transaction has not yet finished.
Another error possibility is that ::http::geturl failed to write the whole of the POST request body (-query or -querychannel data) to the server. ::http::geturl stores the error message for later retrieval by the ::http::postError or ::http::responseInfo commands, and then attempts to complete the transaction. If it can read the server's response the status will be ok, but it is important to call ::http::postError or ::http::responseInfo after every POST to check that the data was sent in full. If the server has closed the connection the status will be eof. π
π METADATA π
β¨ MOST USEFUL METADATA β¨
When a HTTP server responds to a request, it supplies not only the entity requested, but also metadata. This is provided by the first line (the "status line") of the response, and by a number of HTTP headers. Further metadata relates to how ::http::geturl has processed the response from the server.
The most important metadata can be accessed with the command ::http::responseInfo. This command returns a dict of metadata that are essential for identifying a successful transaction and making use of the response, along with other metadata that are informational. The keys of the dict are:
===== Essential Values =====
stageβ set by::http::geturl, this one tells you where in the journey the transaction is rn. The values, in order of the transaction's whole life cycle, are:"created","connecting","header","body", and"complete". Heads up bestie: the other dict keys will not be available untilstagehits"body"or"complete". AndcurrentSizeonly has its final value oncestageis"complete". πstatusβ set by::http::geturl, it's"ok"for a slay successful transaction;"eof","error","timeout", or"reset"for an unsuccessful one; or""if the transaction is still in progress. Same value as what command::http::statusgives you. The meaning of these values is described in the section ERRORS (above).responseCodeβ the "HTTP status code" the server sent in the first line (the "status line") of the response. If it can't be extracted from the status line, the full status line is returned instead.reasonPhraseβ the "reason phrase" the server sent as a description of the HTTP status code. If it can't be extracted from the status line, the full status line is returned. (same energy as above fr fr π«Ά)contentTypeβ the value of the Content-Type response header or, if that header wasn't supplied at all, the default value"application/octet-stream".binaryβ this boolean value, set by::http::geturl, describes how the command interpreted the entity the server returned (after decoding any compression specified by the Content-Encoding response header). That decoded entity is accessible as the return value of the command::http::responseBody.
The value is true if http interpreted the decoded entity as binary. The value returned by ::http::responseBody is a Tcl binary string then. That's the ideal format for image data, zip files, etc. β¨ ::http::geturl picks this value if the user asked for a binary interpretation by passing the -binary option to the command, or if the server supplied a binary content type in a Content-Type response header, or if the server didn't supply any Content-Type header at all.
The value is false in other cases, which means http interpreted the decoded entity as text. The text has been converted, from the character set the server told us about, into Tcl's internal Unicode format; the value returned by ::http::responseBody is an ordinary Tcl string.
Lowkey important: it is always worth checking the value of "binary" after a HTTP transaction, to figure out whether a misconfigured server caused http to interpret a text resource as a binary, or the other way around. π©·
redirectionβ the URL that's the redirection target. The value is that of the Location response header. This header gets sent when a response has status code 3XX (redirection).upgradeβ if it's not empty, the value tells you the protocol(s) the server will switch to after this transaction completes, while continuing to use the same connection. When the server intends to switch protocols, it'll also send the value"101"as the status code (theresponseCodekey), and the word"upgrade"as an element of the Connection response header (theconnectionResponsekey), and it will not send a response body. See the section PROTOCOL UPGRADES for more. π₯errorβ the error message, if there is one. Further info, including a stack trace and error code, is available from command::http::error.postErrorβ the error message (if any) generated when a HTTP POST request sends its request-body to the server. Further info, including a stack trace and error code, is available from command::http::postError. A POST transaction may look complete, according to the keysstage,status, andresponseCode, but bestie it is genuinely important to check thispostErrorkey in case an error happened while uploading the request-body. π
β¨ Informational Values β¨
methodβ the HTTP method used in the request.charsetβ the value of the charset attribute of the Content-Type response header. The charset value is used only for a text resource. If the server didn't specify a charset, the value defaults to that of the variable::http::defaultCharset, which β unless it's been deliberately modified by the caller β isiso8859-1. Incoming text data is automatically converted from the character set defined bycharsetto Tcl's internal Unicode representation, i.e. to a Tcl string. π§compressionβ a copy of the Content-Encoding response-header value.httpRequestβ the version of HTTP specified in the request (i.e. sent in the request line). The value is that of the option-protocolsupplied to::http::geturl(default value"1.1"), unless the command reduced the value to"1.0"because it was passed the-handleroption.httpResponseβ the version of HTTP used by the server (obtained from the response "status line"). The server uses this version of HTTP in its response, but makes sure the response is compatible with the HTTP version specified in the client's request. If the value can't be extracted from the status line, the full status line is returned.urlβ the requested URL, typically the URL supplied as an argument to::http::geturlbut without its "fragment" (the final part of the URL beginning with"#"). πconnectionRequestβ the value, if any, sent to the server in Connection request header(s).connectionResponseβ the value, if any, received from the server in Connection response header(s).connectionActualβ set by::http::geturl, this reports whether the connection was closed after the transaction (value"close"), or left open (value"keep-alive").transferEncodingβ the value of the Transfer-Encoding response header, if it's present. The value is either"chunked"(which means HTTP/1.1 "chunked encoding") or the empty string.totalPostβ the total length of the request body in a POST request.currentPostβ the number of bytes of the POST request body sent to the server so far. Same value as what command::http::sizereturns.totalSizeβ a copy of the Content-Length response-header value. The number of bytes specified in a Content-Length header, if one was sent. If none was sent, the value is0. A correctly configured server omits this header if the transfer-encoding is"chunked", or (for older servers) if the server closes the connection when it reaches the end of the resource.currentSizeβ the number of bytes fetched from the server so far.
π MORE METADATA π
The dictionary returned by ::http::responseInfo is the most useful subset of the available metadata. Other metadata include:
- The full "status line" of the response, available as the return value of command
::http::responseLine. - The full response headers, available as the return value of command
::http::responseHeaders. This return value is a list of the response-header names and values, in the order that they were received from the server.
The return value is not a dict because some header names may occur more than once, notably Set-Cookie. If the value is read into a dict or into an array (using array set), only the last header with each name will be preserved. π
Some of the header names (metadata keys) are listed below, but the HTTP standard defines several more, and servers are free to add their own. When a dictionary key is mentioned below, this refers to the dict value returned by command ::http::responseInfo.
Content-Typeβ the content type of the URL contents. Examples includetext/html,image/gif,application/postscriptandapplication/x-tcl. Text values typically specify a character set, e.g.text/html; charset=UTF-8. Dictionary keycontentType.Content-Lengthβ the advertised size in bytes of the contents, available as dictionary keytotalSize. The actual number of bytes read by::http::geturlso far is available as dictionary keycurrentSize.Content-Encodingβ the compression algorithm used for the contents. Examples includegzip,deflate. Dictionary keycontent.Locationβ this header is sent when a response has status code 3XX (redirection). It provides the URL that is the redirection target. Dictionary keyredirection.Set-Cookieβ this header is sent to offer a cookie to the client. Cookie management is done by the::http::configoption-cookiejar, and so theSet-Cookieheaders need not be parsed by user scripts. See section COOKIE JAR PROTOCOL. πConnectionβ the value can be supplied as a comma-separated list, or by multiple headers. The list often has only one element, either"close"or"keep-alive". The value"upgrade"indicates a successful upgrade request and is typically combined with the status code101, anUpgraderesponse header, and no response body. Dictionary keyconnectionResponse.Upgradeβ the value indicates the protocol(s) to which the server will switch immediately after the empty line that terminates the101response headers. Dictionary keyupgrade.
π EVEN MORE METADATA π
- Details of the HTTP request. The request is determined by the options supplied to
::http::geturland::http::config. However, it's sometimes helpful to examine what::http::geturlactually sent to the server, and this information is available through commands::http::requestHeadersand::http::requestLine. π - The state array: the internal variables of
::http::geturl. It may sometimes be helpful to examine this array. Details are given in the next section. π¦
πΈ STATE ARRAY πΈ
okay bestie so here's the tea π« β when you call ::http::geturl, it hands you back a token, right? and that token? it's not just a lil receipt, it's a whole VIP pass ποΈ. You can feed it to all the other ::http::* commands, and they'll poke at the state of your HTTP transaction for you. For most vibes, those commands alone are plenty, no cap.
But β¨plot twistβ¨ β that token can also unlock the transaction's internal state, which lives inside a Tcl array. This is the good stuff when you're out here writing callback commands for the options -command, -handler, -progress, or -queryprogress. iykyk. π
So inside your proc, drop this line to make a super convenient local variable called state that maps straight onto that array:
upvar 0 $token state
iconic behavior honestly.
And listen β once you're done with the data hanging off that URL, unset the state array to free up the storage. Don't just leave it lying around like last week's iced coffee π§. That's literally what ::http::cleanup is for, go use it.
The array elements below are the origin of the values that the commands return, so this is the source-of-truth moment. π And whenever a dictionary key gets mentioned? That means the dict value you get back from ::http::responseInfo. Slay, let's go:
binaryβ this one's for dictionary keybinary.bodyβ feeds command::http::responseBody.charsetβ for dictionary keycharset.codingβ for dictionary keycompression.connectionβ for dictionary keyconnectionActual.currentsizeβ for command::http::size; and for dictionary keycurrentSize.errorβ for command::http::error; thepartbit gets used in dictionary keyerror.httpβ for command::http::responseLine.httpResponseβ for dictionary keyhttpResponse.metaβ for command::http::responseHeaders. (there's more chatting about this up in the section MORE METADATA π)methodβ for dictionary keymethod.posterrorβ for dictionary keypostError.postErrorFullβ for command::http::postError.-protocolβ for dictionary keyhttpRequest.querylengthβ for dictionary keytotalPost.queryoffsetβ for dictionary keycurrentPost.reasonPhraseβ for dictionary keyreasonPhrase.requestHeadersβ for command::http::requestHeaders.requestLineβ for command::http::requestLine.responseCodeβ for dictionary keyresponseCode.stateβ for dictionary keystage.statusβ for command::http::status; and for dictionary keystatus.totalsizeβ for dictionary keytotalSize.transferβ for dictionary keytransferEncoding.typeβ for dictionary keycontentType.upgradeβ for dictionary keyupgrade.urlβ for dictionary keyurl.
periodt. π
π PERSISTENT CONNECTIONS π«
π± BASICS
okay so first things first, bestie: go read RFC 7230 Sec 6, which is the glow-up that supersedes RFC 2616 Sec 8.1. that's the canon, that's the source material, periodt.
so what is a persistent connection? it's when multiple HTTP/1.1 transactions ride the same TCP connection like besties sharing one aux cord. and pipelining? that's when a client fires off a bunch of requests over a persistent connection without waiting for each response to come back first. the server then sends the responses back in the exact same order the requests arrived β no cutting the line, okay? πΆβοΈπΆβοΈπΆβοΈ
now, a lil real talk π«Ά: if a POST request fails to complete, you typically need the user to confirm before you fire that request off again. and honestly? the user might wanna check whether the server got modified by that failed POST first, before re-sending the exact same request. cautious queen behavior, we love it.
an HTTP request will use a persistent socket if the call to http::geturl has the option -keepalive true. it'll use pipelining where permitted if the http::config option -pipeline is boolean true (which is its default value, so yes, it's*on* unless you say otherwise π).
the http package keeps no more than one persistent connection to each server β and by "each server" we mean each value of "domain:port". so if http::geturl gets called to make a request over a persistent connection while that connection is busy with another request? no stress, the new request just waits in a queue until the connection is free. it's giving patient. it's giving waiting-room energy. πͺ
and one big disclaimer: the http package does not support HTTP/1.0 persistent connections controlled by the Keep-Alive header. nope. not doing it. π
βοΈ
π¨ SPECIAL CASES
this subsection is where we talk about the server ghosting your connection π», automatic retry of failed requests, the special treatment POST requests need, and the options for dealing with all of it.
in accordance with RFC 7230, http::geturl does not pipeline requests that use the POST method. so if a POST uses a persistent connection and is not the first request on that connection, http::geturl waits until it's received the response for the previous request; or β if http::config option -postfresh is boolean true β it just uses a new connection for each POST. fresh connection, fresh start, self-care π§βοΈ.
if the server is processing a bunch of pipelined requests and sends a response header Connection: close with one of the responses (any of them other than the last one), then the subsequent responses are unfulfilled π. http::geturl will send the unfulfilled requests again over a new connection, so it's not a total loss.
okay here's where it gets spicy πΆοΈ. a difficulty arises when an HTTP client sends a request over a persistent connection that's been idle for a while. the HTTP server may half-close an apparently idle connection while the client is sending a request, but before the request actually arrives at the server. in that case β an "asynchronous close event" β the request will fail. the difficulty is that the client cannot be certain whether the POST modified the state of the server. like, did it go through or not?? nobody knows!! for HEAD or GET requests, http::geturl just opens another connection and retransmits the failed request. easy. but if the request was a POST, RFC 7230 forbids automatic retry by default, suggesting either user confirmation, or confirmation by user-agent software that has semantic understanding of the application. the http::config option -repost allows for either possibility. iykyk.
asynchronous close events can occur only in a short interval of time, so don't spiral about it. the http package monitors each persistent connection for closure by the server. and the second it's detected? the connection is also closed at the client end, and subsequent requests will use a fresh connection. bye felicia π
if the http::geturl command is called with option -keepalive true, then it will both try to use an existing persistent connection (if one's available), and it'll send the server a Connection: keep-alive request header asking to keep the connection open for future requests. polite! considerate! iconic! π
the http::config options -pipeline, -postfresh, and -repost are the ones relating to persistent connections. let's break them down:
-pipelineβ if boolean true, will pipelineGETandHEADrequests made over a persistent connection.POSTrequests will not be pipelined: if thePOSTis not the first transaction on the connection, its request will not be sent until the previous response has finished. andGETandHEADrequests made after aPOSTwill not be sent until thePOSTresponse has been delivered, and will not be sent at all if thePOSTfails. π€-postfreshβ if boolean true, will override thehttp::geturloption-keepalive, and always open a fresh connection for aPOSTrequest.-repostβ if true, permits automatic retry of aPOSTrequest that fails because it uses a persistent connection that the server has half-closed (an "asynchronous close event"). subsequentGETandHEADrequests in a failed pipeline will also be retried.
β οΈ bestie listen to me on this one: the-repostoption should be used only if the application understands that the retry is appropriate. specifically, the application must know that if the failedPOSTsuccessfully modified the state of the server, a repeatPOSTwould have no adverse effect. do not just turn this on and hope. that's not the vibe. π
πͺ COOKIE JAR PROTOCOL
cookies are short key-value pairs used to implement sessions within the otherwise-stateless HTTP protocol. (see RFC 6265 for all the details; Tcl does not implement the Cookie2 protocol as that is rarely seen in the wild π¦.)
cookie storage management commands β "cookie jars" β must support these subcommands which form the HTTP cookie storage management protocol. quick note: cookieJar below does not have to be a command name; it is properly a command prefix (a Tcl list of words that will be expanded in place) and admits many possible implementations. so flexible. so demure. β¨
though not formally part of the protocol, it's expected that particular values of cookieJar will correspond to sessions; it's up to the caller of ::http::config to decide what session applies and to manage the deletion of said sessions when they're no longer desired (which should be when they're not configured as the current cookie jar). clean up after yourself, that's the rule π§Ή
cookieJar getCookies protocol host requestPathβ this command asks the cookie jar what cookies should be supplied for a particular request. it should take the protocol (typicallyhttporhttps), host name and requestPath (parsed from the url argument to::http::geturl) and return a list of cookie keys and values that describe the cookies to supply to the remote host. the list must have an even number of elements, no cap.
there should only ever be at most one cookie with a particular key for any request (typically the one with the most specific host/domain match and most specific requestPath/path match), but there may be many cookies with different names in any request.
cookieJar storeCookie cookieDictionaryβ this command asks the cookie jar to store a particular cookie that was returned by a request; the result of this command is ignored. the cookie (which will have been parsed by the http package) is described by a dictionary, cookieDictionary, that may have the following keys:domainβ this is always present. its value describes the domain hostname or prefix that the cookie should be returned for. the checking of the domain against the origin (below) should be careful since sites that issue cookies should only do so for domains related to themselves. cookies that do not obey a relevant origin matching rule should be ignored. π«expiresβ this is optional. if present, the cookie is intended to be a persistent cookie and the value of the option is the Tcl timestamp (in seconds from the same base asclock seconds) of when the cookie expires (which may be in the past, which should result in the cookie being deleted immediately). if absent, the cookie is intended to be a session cookie that should not be persisted beyond the lifetime of the cookie jar.hostonlyβ this is always present. its value is a boolean that describes whether the cookie is a single host cookie (true) or a domain-level cookie (false).httponlyβ this is always present. its value is a boolean that is true when the site wishes the cookie to only ever be used with HTTP (or HTTPS) traffic.keyβ this is always present. its value is the key of the cookie, which is part of the information that must be returned when sending this cookie back in a future request.originβ this is always present. its value describes where the http package believes it received the cookie from, which may be useful for checking whether the cookie's domain is valid.pathβ this is always present. its value describes the path prefix of requests to the cookie domain where the cookie should be returned.secureβ this is always present. its value is a boolean that is true when the cookie should only be used on requests sent over secure channels (typically HTTPS). πvalueβ this is always present. its value is the value of the cookie, which is part of the information that must be returned when sending this cookie back in a future request.- other keys may always be ignored; they have no meaning in this protocol. π€·βοΈ
π PROTOCOL UPGRADES π
okay bestie, listen β sometimes you start a convo with a server over plain old HTTP and then you're like "actuallyβ¦ can we be different?" and that's literally what protocol upgrades are π¦
The HTTP/1.1 Connection and Upgrade request headers tell the server that the client wants to switch up the protocol being used on the connection that's already open (that's RFC 7230, for the citation girlies). You can use this mechanism to ask for a WebSocket (RFC 6455), a higher version of HTTP (HTTP 2), or TLS encryption. And if the server is like "yes bestie, slay"? The response code comes back as 101. π
To request a protocol upgrade when you call http::geturl, you've got two jobs:
- the
-headersoption must supply appropriate values forConnectionandUpgrade, and - the
-commandoption must supply a command that implements the protocol you asked for β and that command also has to be able to handle the server response in case the server refuses the upgrade. π
For upgrade requests, http::geturl ignores whatever you put in the -keepalive option, and always uses the value 0, so that the upgrade request doesn't go out over a connection that's meant for multiple HTTP requests. periodt.
The Tcllib library websocket implements WebSockets, and it makes the necessary calls to commands in the http package. π
heads up: there is currently no native Tcl client library for HTTP/2. π
So why is the Upgrade mechanism not the way browsers ask for TLS? Because http and https are served over different ports, that's why. It is used by protocols like Internet Printing Protocol (IPP) that are built on top of http(s) and use the same TCP port number for both secure and insecure traffic. iykyk.
In browsers, opportunistic encryption is instead implemented by the Upgrade-Insecure-Requests client header. If a secure service is available, the server response code is a 307 redirect, and the response header Location specifies the target URL. The browser then has to call http::geturl again to go fetch that URL. See https://w3c.github.io/webappsec-upgrade-insecure-requests/ β¨
π§΅ THREADS π§΅
π― PURPOSE π―
The command ::http::geturl uses the Tcl ::socket command with the -async option to connect to a remote server β but ngl, the return from that command can be delayed in adverse cases (like a slow DNS lookup π΄), and that delay prevents the event loop from processing other events. Not the vibe.
The delay is avoided if the ::socket command is evaluated in another thread. π«Ά The Thread package is not part of Tcl, but it is provided in "Batteries Included" distributions. And instead of the ::socket command, the http package uses ::http::socket, which makes connections in the manner specified by the value of -threadlevel and the availability of package Thread.
π WITH TLS (HTTPS) π
The same -threadlevel configuration applies to both HTTP and HTTPS connections. HTTPS gets enabled by using the http::register command β typically by specifying the ::tls::socket command of the tls package to handle TLS cryptography. π₯
Now the β¨loreβ¨: the ::tls::socket command connects to the remote server by using the command specified by the value of variable ::tls::socketCmd, and that value defaults to "::socket". If http::geturl finds that ::tls::socketCmd has this value, it replaces it with the value "::http::socket". But if ::tls::socketCmd has a value other than "::socket" β i.e. if the script or the Tcl installation has replaced the value "::socket" with the name of a different command β then http does not change the value. In that case, whoever modified ::tls::socketCmd (the script or the installation) is responsible for integrating ::http::socket into their own replacement command. you broke it, you fix it bestie π
πΆ WITH A CHILD INTERPRETER πΆ
okay this one's a lil specific: the peer thread can transfer the socket only to the main interpreter of the script's thread. So the thread-based ::http::socket works with non-zero -threadlevel values only if the script runs in the main interpreter. π¨
A child interpreter must use -threadlevel 0 unless the parent interpreter has provided alternative facilities. The main parent interpreter may grant full -threadlevel facilities to a child interpreter β for example by aliasing, in the child, to ::http::socket, a command that runs http::socket in the parent, and then transfers the socket to the child. iconic parent behaviour tbh π
π EXAMPLE π
This example creates a procedure to copy a URL to a file while printing a progress meter, and prints the response headers associated with the URL. it's giving multitasking π
proc httpcopy { url file {chunk 4096} } {
set out [open $file w]
set token [::http::geturl $url -channel $out \
-progress httpCopyProgress -blocksize $chunk]
close $out
# This ends the line started by httpCopyProgress
puts stderr ""
upvar 0 $token state
set max 0
foreach {name value} $state(meta) {
if {[string length $name] > $max} {
set max [string length $name]
}
if {[regexp -nocase ^location$ $name]} {
# Handle URL redirects
puts stderr "Location:$value"
return [httpcopy [string trim $value] $file $chunk]
}
}
incr max
foreach {name value} $state(meta) {
puts [format "%-*s %s" $max $name: $value]
}
return $token
}
proc httpCopyProgress {args} {
puts -nonewline stderr .
flush stderr
}
π SEE ALSO π
safe(3tcl)socket(3tcl)safesock(3tcl)
π KEYWORDS π
internet, security policy, socket, www
and that's a wrap on the whole page bestie!! π this lil masterpiece is http, version 2.10, living in the http(3tcl) manpage family β go forth and make requests responsibly, no cap ππ¦β¨
π¦ where this page comes from
- ubuntu stonking: tcl9.0-doc 9.0.4+dfsg-1
- Copyright (c) 1995-1997 Sun Microsystems, Inc
- Copyright (c) 1998-2000 Ajuba Solutions
- Copyright (c) 2004 ActiveState Corporation
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms