lib/async/http/protocol/http1/connection.rb



# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2018-2026, by Samuel Williams.

require_relative "request"
require_relative "response"

require "protocol/http1"
require "protocol/http/peer"

module Async
	module HTTP
		module Protocol
			module HTTP1
				# An HTTP/1 connection that wraps an IO stream with version and state tracking.
				class Connection < ::Protocol::HTTP1::Connection
					# Initialize the connection with an IO stream and HTTP version.
					# @parameter stream [IO::Stream] The underlying stream.
					# @parameter version [String] The negotiated HTTP version string.
					# @parameter options [Hash] Additional options for the connection.
					def initialize(stream, version, **options)
						super(stream, **options)
						
						# On the client side, we need to send the HTTP version with the initial request. On the server side, there are some scenarios (bad request) where we don't know the request version. In those cases, we use this value, which is either hard coded based on the protocol being used, OR could be negotiated during the connection setup (e.g. ALPN).
						@version = version
					end
					
					# @returns [String] A string representation of this connection.
					def to_s
						"\#<#{self.class} negotiated #{@version}, #{@state}>"
					end
					
					# @returns [String] A JSON-compatible representation.
					def as_json(...)
						to_s
					end
					
					# @returns [String] A JSON string representation.
					def to_json(...)
						as_json.to_json(...)
					end
					
					attr :version
					
					# @returns [Boolean] Whether this is an HTTP/1 connection.
					def http1?
						true
					end
					
					# @returns [Boolean] Whether this is an HTTP/2 connection.
					def http2?
						false
					end
					
					# @returns [Protocol::HTTP::Peer] The peer information for this connection.
					def peer
						@peer ||= ::Protocol::HTTP::Peer.for(@stream.io)
					end
					
					attr :count
					
					# @returns [Integer] The maximum number of concurrent requests (always 1 for HTTP/1).
					def concurrency
						1
					end
					
					# Can we use this connection to make requests?
					def viable?
						unless self.idle?
							return false
						end
						
						unless @stream
							return false
						end
						
						# `nil` indicates that the connection has no data, but is still alive. An empty string means the connection is closed, while a non-empty string indicates application data on a idle HTTP/1 connection, both are failure cases.
						if @stream.peek_partial(1)
							return false
						end
						
						return @stream.readable?
					rescue => error
						Console.debug(self, "Connection viability probe failed!", exception: error)
						
						return false
					end
					
					# @returns [Boolean] Whether the connection can be reused for another request.
					def reusable?
						@persistent && @stream && !@stream.closed?
					end
				end
			end
		end
	end
end