nethttp

package module
v0.0.0-...-ce41505 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-ruby-net-http/net-http

net-http — go-ruby-net-http

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's Net::HTTP HTTP/1.1 message codec — the deterministic, interpreter-independent core of MRI 4.0.5's Net::HTTP: it builds request bytes exactly as MRI writes them to the socket, parses a raw HTTP/1.1 response byte stream into MRI's Net::HTTPResponse subclass model, and ports the Net::HTTPHeader mixin — without any Ruby runtime, and without performing any I/O itself.

It is the HTTP-message backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (the Psych codec).

The socket / TLS is a host-side seam. Building HTTP/1.1 request bytes (request line, default headers, Content-Length / chunked framing, form and basic-auth encoding) and parsing a response byte stream (status line, folded multi-value headers, Content-Length and chunked decoding, the response subclass selected by status code) is fully deterministic and needs no interpreter, so it lives here as pure Go. Opening the TCPSocket, doing the TLS handshake, and reading/writing the bytes is the host's job (rbgo supplies the byte transport): hand Request.Bytes to your socket's write, and feed everything read back from the socket to ParseResponse.

Features

Faithful port of Net::HTTP's request build + response parse, validated against the ruby binary on every supported platform:

  • Request building for Get / Post / Put / Delete / Head / Patch / Options — the request line, MRI's default headers in MRI's exact order and casing (Accept-Encoding, Accept, User-Agent, Host), Content-Length vs. Transfer-Encoding handling, the empty-body default for body-permitting methods, set_form_data (application/x-www-form-urlencoded), and HTTP Basic / Proxy-Basic auth — byte-for-byte identical to MRI's socket writes.
  • Response parsing of a raw HTTP/1.1 byte stream — the status line (/\AHTTP(?:\/\d+\.\d+)?\s+\d\d\d(?:\s+.*)?\z/in), header lines with obs-fold continuation and repeated multi-value fields, and the body decoded by Content-Length or chunked Transfer-Encoding (chunk extensions and trailers included).
  • The Net::HTTPResponse subclass hierarchy — every status code mapped to its subclass (HTTPOK, HTTPNotFound, HTTPMovedPermanently, …) and category (HTTPSuccess / HTTPRedirection / HTTPClientError / HTTPServerError / HTTPInformation), generated from MRI's own CODE_TO_OBJ table, with the HAS_BODY rule (1xx, 204, 205, 304 carry no body) and the CODE_CLASS_TO_OBJ / HTTPUnknownResponse fallbacks.
  • The Net::HTTPHeader mixin[] / []= / key? / delete / add_field / get_fields / each_header / each_capitalized, plus content_type / content_length / chunked? / connection_close? and the URI.encode_www_form component encoder.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three operating systems (Linux, macOS, Windows).

Install

go get github.com/go-ruby-net-http/net-http

Usage

package main

import (
	"fmt"

	nethttp "github.com/go-ruby-net-http/net-http"
)

func main() {
	// Build the request bytes (Net::HTTP::Post.new + set_form_data).
	req, _ := nethttp.NewRequest("POST", "/submit", "example.com", nil)
	req.SetFormData([][2]string{{"name", "a b"}, {"x", "1&2"}})
	wire, _ := req.Bytes("1.1")
	// POST /submit HTTP/1.1
	// Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
	// Accept: */*
	// User-Agent: Ruby
	// Host: example.com
	// Content-Type: application/x-www-form-urlencoded
	// Content-Length: 16
	//
	// name=a+b&x=1%262
	//
	// ... the host writes `wire` to its connected (TLS) socket, then reads the
	// whole response back and hands the bytes to ParseResponse:

	raw := "HTTP/1.1 200 OK\r\n" +
		"Content-Type: text/plain\r\n" +
		"Transfer-Encoding: chunked\r\n\r\n" +
		"4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n"
	res, _ := nethttp.ParseResponse([]byte(raw))
	fmt.Println(res.Class(), res.Code(), res.Message()) // HTTPOK 200 OK
	fmt.Println(res.IsSuccess())                        // true
	fmt.Printf("%q\n", res.Body())                      // "Wikipedia" (chunked-decoded)
	_ = wire
}

The socket / TLS seam

This library is the message codec only; the transport is the host's:

Stage Owner This library
DNS, TCPSocket, TLS host (rbgo)
serialise the request this library Request.Bytes(version) []byte
write bytes to the socket host (rbgo)
read bytes from the socket host (rbgo)
parse the response this library ParseResponse([]byte) (*Response, error)

Request.Bytes is the inverse of ParseResponse. Neither touches the network, a file, or a clock, so the codec is fully deterministic and testable in isolation — exactly how the host can drive it from any byte transport.

API

// Request building (Net::HTTPGenericRequest + the Get/Post/... subclasses).
func NewRequest(method, path, host string, initHeader [][2]string) (*Request, error)
func (r *Request) Bytes(version string) ([]byte, error) // exact MRI socket bytes
func (r *Request) SetBody(body []byte)
func (r *Request) SetFormData(params [][2]string, sep ...string) // set_form_data
func (r *Request) BasicAuth(account, password string)           // basic_auth
func (r *Request) ProxyBasicAuth(account, password string)
func (r *Request) Method() string
func (r *Request) Path() string
func (r *Request) RequestBodyPermitted() bool
func (r *Request) ResponseBodyPermitted() bool

// Response parsing (Net::HTTPResponse.read_new + read_body).
func ParseResponse(raw []byte) (*Response, error)
func (r *Response) Code() string        // "200"
func (r *Response) Message() string     // "OK"
func (r *Response) HTTPVersion() string // "1.1"
func (r *Response) Class() string       // "HTTPOK"
func (r *Response) Category() string    // "HTTPSuccess"
func (r *Response) Body() []byte        // decoded (Content-Length or chunked)
func (r *Response) IsSuccess() bool     // kind_of? Net::HTTPSuccess (+ Is{Information,Redirection,ClientError,ServerError})

// The Net::HTTPHeader mixin, embedded in both Request and Response.
func (h *Header) Get(key string) (string, bool)        // []
func (h *Header) Set(key, val string) error            // []=
func (h *Header) AddField(key, val string) error       // add_field
func (h *Header) GetFields(key string) []string        // get_fields
func (h *Header) EachHeader(fn func(key, value string)) // each_header
func (h *Header) ContentType() string                  // content_type
func (h *Header) ContentLength() (int, bool, error)    // content_length
func (h *Header) Chunked() bool                        // chunked?
func (h *Header) ConnectionClose() bool                // connection_close?

// URI.encode_www_form helpers.
func EncodeWWWFormComponent(s string) string
func EncodeWWWForm(pairs [][2]string) string

Response subclass model

Like MRI, a parsed response carries the subclass identity its status selects — exposed as Class() / Category() and the Is* kind predicates rather than a distinct Go type per code:

Code(s) Class() Category() Body?
200 HTTPOK HTTPSuccess yes
204 / 304 HTTPNoContent / HTTPNotModified HTTPSuccess / HTTPRedirection no
301 HTTPMovedPermanently HTTPRedirection yes
404 HTTPNotFound HTTPClientError yes
500 HTTPInternalServerError HTTPServerError yes
unknown 2xx (299) HTTPSuccess HTTPSuccess yes
unknown (999) HTTPUnknownResponse HTTPUnknownResponse yes

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: the same requests are serialised here and by the system ruby (Net::HTTPGenericRequest#exec writing to a recording socket) and compared byte-for-byte; responses are parsed both here and by Net::HTTPResponse.read_new (status, multi-value headers, chunked-decoded body, selected subclass over MRI's whole CODE_TO_OBJ table) and compared. The oracle scripts $stdout.binmode so Windows text-mode never pollutes the bytes, and skip themselves where ruby is absent.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-net-http/net-http authors.

Documentation

Overview

Package nethttp is a pure-Go (no cgo) reimplementation of the deterministic, interpreter-independent core of Ruby's Net::HTTP: the HTTP/1.1 *message* codec. It builds request bytes exactly as MRI 4.0.5 writes them to the socket and parses a raw response byte stream into MRI's Net::HTTPResponse subclass model — without any Ruby runtime, and without performing any I/O itself.

The TCP socket and TLS are a host-side seam: the host supplies the byte transport, this library supplies build-request-bytes + parse-response-bytes + the header / response object model.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EncodeWWWForm

func EncodeWWWForm(pairs [][2]string) string

EncodeWWWForm joins key/value pairs into an application/x-www-form-urlencoded query string, mirroring URI.encode_www_form. Each key and value is encoded with EncodeWWWFormComponent and joined with '='; pairs are joined with '&'. A pair whose value is the empty string is emitted as bare "key" only when it was given as a value-less entry; here every pair has a value, so an empty value yields "key=" — matching MRI for the [][2]string form.

func EncodeWWWFormComponent

func EncodeWWWFormComponent(s string) string

EncodeWWWFormComponent percent-encodes s as URI.encode_www_form_component does: unreserved bytes pass through, space becomes '+', and every other byte becomes %XX with uppercase hex.

Types

type BadResponse

type BadResponse struct{ Msg string }

BadResponse corresponds to Net::HTTPBadResponse: a protocol-level error while parsing the status line, a header line, or a chunk-size line.

func (*BadResponse) Error

func (e *BadResponse) Error() string
type Header struct {
	// contains filtered or unexported fields
}

Header is the field store shared by requests and responses — the pure-Go port of the Net::HTTPHeader mixin. Like MRI it stores keys downcased, each mapping to an ordered list of raw string values, and preserves first-insertion order.

func NewHeader

func NewHeader() *Header

NewHeader returns an empty Header.

func (*Header) AddField

func (h *Header) AddField(key, val string) error

AddField appends val to key, preserving any existing values (Net::HTTPHeader#add_field).

func (*Header) Chunked

func (h *Header) Chunked() bool

Chunked reports whether Transfer-Encoding requests chunked encoding (Net::HTTPHeader#chunked?). MRI uses /(?:\A|[^\-\w])chunked(?![\-\w])/i.

func (*Header) ConnectionClose

func (h *Header) ConnectionClose() bool

ConnectionClose reports whether Connection (or Proxy-Connection) requests the connection be closed (Net::HTTPHeader#connection_close?).

func (*Header) ConnectionKeepAlive

func (h *Header) ConnectionKeepAlive() bool

ConnectionKeepAlive reports whether Connection (or Proxy-Connection) requests keep-alive (Net::HTTPHeader#connection_keep_alive?).

func (*Header) ContentLength

func (h *Header) ContentLength() (int, bool, error)

ContentLength returns the parsed Content-Length, ok=false when the field is absent (Net::HTTPHeader#content_length). A present-but-malformed value yields a *HeaderSyntaxError.

func (*Header) ContentType

func (h *Header) ContentType() string

ContentType returns "main/sub" (or just "main", or "") mirroring Net::HTTPHeader#content_type.

func (*Header) Delete

func (h *Header) Delete(key string) []string

Delete removes key and returns its prior value list (Net::HTTPHeader#delete).

func (*Header) EachCapitalized

func (h *Header) EachCapitalized(fn func(key, value string))

EachCapitalized calls fn for each field in insertion order with the canonicalised key and the joined value (Net::HTTPHeader#each_capitalized) — this is the order and casing written to the wire by write_header.

func (*Header) EachHeader

func (h *Header) EachHeader(fn func(key, value string))

EachHeader calls fn for each field in insertion order with the downcased key and the values joined by ", " (Net::HTTPHeader#each_header / #each).

func (*Header) Get

func (h *Header) Get(key string) (string, bool)

Get returns the field for key joined by ", " (Net::HTTPHeader#[]), and "" with ok=false when the field is absent.

func (*Header) GetFields

func (h *Header) GetFields(key string) []string

GetFields returns a copy of the raw value list for key, or nil if absent (Net::HTTPHeader#get_fields).

func (*Header) Key

func (h *Header) Key(key string) bool

Key reports whether key is present (Net::HTTPHeader#key?).

func (*Header) MainType

func (h *Header) MainType() string

MainType returns the part of Content-Type before '/' (Net::HTTPHeader#main_type), or "" if there is no Content-Type.

func (*Header) Set

func (h *Header) Set(key, val string) error

Set replaces the field for key (Net::HTTPHeader#[]=). A nil-equivalent is expressed by Delete; Set always stores a value.

func (*Header) SetContentLength

func (h *Header) SetContentLength(n int)

SetContentLength sets Content-Length to len (Net::HTTPHeader#content_length=).

func (*Header) SetContentType

func (h *Header) SetContentType(typ string, params ...[2]string)

SetContentType sets Content-Type to type plus "; k=v" params in the given order (Net::HTTPHeader#set_content_type / #content_type=).

func (*Header) SubType

func (h *Header) SubType() string

SubType returns the part of Content-Type after '/' (Net::HTTPHeader#sub_type), or "" if there is none.

type HeaderSyntaxError

type HeaderSyntaxError struct{ Msg string }

HeaderSyntaxError corresponds to Net::HTTPHeaderSyntaxError: a malformed Content-Length / Content-Range field value.

func (*HeaderSyntaxError) Error

func (e *HeaderSyntaxError) Error() string

type Request

type Request struct {
	*Header
	// contains filtered or unexported fields
}

Request is the pure-Go port of Net::HTTPGenericRequest (the parent of every Net::HTTP::Get/Post/... request). It carries the method, the request path, the header model, and an optional body, and serialises itself to the exact HTTP/1.1 request bytes MRI writes to the socket.

func NewRequest

func NewRequest(method, path, host string, initHeader [][2]string) (*Request, error)

NewRequest builds a request for the given uppercase method ("GET", "POST", …) targeting path. path is the request-target as written on the request line (e.g. URI#request_uri: "/p?q=1"); host, when non-empty, supplies the default Host header (URI#authority). initHeader is the ordered list of caller-supplied initial fields; MRI preserves the initheader hash's insertion order on the wire, so the order of these pairs is the order they are emitted.

It mirrors Net::HTTPGenericRequest#initialize: seeding Accept-Encoding (unless the caller's initheader already set accept-encoding or range) appended after the caller's fields, then the Accept, User-Agent and Host defaults.

func (*Request) BasicAuth

func (r *Request) BasicAuth(account, password string)

BasicAuth sets the Authorization header to a Basic credential for account/password (Net::HTTPHeader#basic_auth / #basic_encode).

func (*Request) Body

func (r *Request) Body() []byte

Body returns the request body, or nil if none was set.

func (*Request) Bytes

func (r *Request) Bytes(version string) ([]byte, error)

Bytes returns the exact HTTP/1.1 request byte stream MRI writes to the socket for this request at the given version (e.g. "1.1"). This is the host-side seam's payload: write these bytes to the connected socket.

It mirrors Net::HTTPGenericRequest#exec: when a body is present (explicitly, or defaulted to "" for a body-permitted method), it sets Content-Length, deletes Transfer-Encoding, writes the header block, then the body — exactly send_request_with_body. Otherwise it writes the header block alone.

func (*Request) Method

func (r *Request) Method() string

Method returns the request method ("GET", "POST", …).

func (*Request) Path

func (r *Request) Path() string

Path returns the request-target path written on the request line.

func (*Request) ProxyBasicAuth

func (r *Request) ProxyBasicAuth(account, password string)

ProxyBasicAuth sets the Proxy-Authorization header (Net::HTTPHeader#proxy_basic_auth).

func (*Request) RequestBodyPermitted

func (r *Request) RequestBodyPermitted() bool

RequestBodyPermitted reports whether the request may carry a body (Net::HTTPGenericRequest#request_body_permitted?).

func (*Request) ResponseBodyPermitted

func (r *Request) ResponseBodyPermitted() bool

ResponseBodyPermitted reports whether a response to this request may carry a body (Net::HTTPGenericRequest#response_body_permitted?).

func (*Request) SetBody

func (r *Request) SetBody(body []byte)

SetBody sets the request body (Net::HTTPGenericRequest#body=).

func (*Request) SetFormData

func (r *Request) SetFormData(params [][2]string, sep ...string)

SetFormData sets an application/x-www-form-urlencoded body from params, mirroring Net::HTTPHeader#set_form_data: it URL-encodes the pairs (default separator "&"), sets the body, and sets Content-Type.

type Response

type Response struct {
	*Header
	// contains filtered or unexported fields
}

Response is the pure-Go port of Net::HTTPResponse: the parsed status line, the response header model (it embeds *Header, so all Net::HTTPHeader getters apply), the decoded body, and the selected subclass identity. The subclass is not a Go type but the Class / Category names MRI would have instantiated (e.g. "HTTPOK" in category "HTTPSuccess"), exposed via Class, Category and the kind helpers.

func ParseResponse

func ParseResponse(raw []byte) (*Response, error)

ParseResponse parses a complete raw HTTP/1.1 response byte stream into a Response: the status line, the header block (with obs-fold continuation and multi-value fields), and the body decoded per Content-Length or chunked Transfer-Encoding. It is the inverse of Request.Bytes and the payload side of the host's read seam: hand it everything read from the socket for one response.

It mirrors Net::HTTPResponse.read_new (status line + headers) followed by read_body (body framing). A response whose status permits no body (HAS_BODY false: 1xx, 204, 205, 304) yields a nil body and ignores any trailing bytes, exactly as MRI does.

func (*Response) Body

func (r *Response) Body() []byte

Body returns the decoded response body (Net::HTTPResponse#body), or nil for a status that permits no body. It is populated by ReadBody during ParseResponse.

func (*Response) BodyPermitted

func (r *Response) BodyPermitted() bool

BodyPermitted reports whether the status permits a body (Net::HTTPResponse.body_permitted? / HAS_BODY).

func (*Response) Category

func (r *Response) Category() string

Category returns the response category class name (e.g. "HTTPSuccess", "HTTPClientError") — the per-first-digit parent class.

func (*Response) Class

func (r *Response) Class() string

Class returns the Net::HTTPResponse subclass name MRI selects for this code (e.g. "HTTPOK", "HTTPNotFound", "HTTPUnknownResponse").

func (*Response) Code

func (r *Response) Code() string

Code returns the 3-digit status code string (Net::HTTPResponse#code).

func (*Response) HTTPVersion

func (r *Response) HTTPVersion() string

HTTPVersion returns the server's HTTP version string (Net::HTTPResponse#http_version).

func (*Response) IsClientError

func (r *Response) IsClientError() bool

IsClientError reports a 4xx response (kind_of? Net::HTTPClientError).

func (*Response) IsInformation

func (r *Response) IsInformation() bool

IsInformation reports a 1xx response (kind_of? Net::HTTPInformation).

func (*Response) IsRedirection

func (r *Response) IsRedirection() bool

IsRedirection reports a 3xx response (kind_of? Net::HTTPRedirection).

func (*Response) IsServerError

func (r *Response) IsServerError() bool

IsServerError reports a 5xx response (kind_of? Net::HTTPServerError).

func (*Response) IsSuccess

func (r *Response) IsSuccess() bool

IsSuccess reports a 2xx response (kind_of? Net::HTTPSuccess).

func (*Response) Message

func (r *Response) Message() string

Message returns the reason phrase (Net::HTTPResponse#message / #msg).

func (*Response) ReadBody

func (r *Response) ReadBody() []byte

ReadBody returns the cached decoded body (Net::HTTPResponse#read_body); the body is decoded once during parsing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL