http API

http

package

API reference for the http package.

S
struct

cacheEntry

cacheEntry holds the result of a fetch operation.

http/http.go:20-25
type cacheEntry struct

Fields

Name Type Description
once sync.Once
data []byte
err error
ready chan struct{}
S
struct

textEntry

http/http.go:26-31
type textEntry struct

Fields

Name Type Description
once sync.Once
text string
err error
ready chan struct{}
F
function

RegisterHTTPHook

RegisterHTTPHook registers fn to receive HTTP request events.

Parameters

fn
func(start bool, url string, status int, duration time.Duration)
http/http.go:42-46
func RegisterHTTPHook(fn func(start bool, url string, status int, duration time.Duration))

{
	httpHookMu.Lock()
	httpHook = fn
	httpHookMu.Unlock()
}
F
function

currentHTTPHook

Returns

func(bool,
string, int, time.Duration)
http/http.go:48-53
func currentHTTPHook() func(bool, string, int, time.Duration)

{
	httpHookMu.RLock()
	hook := httpHook
	httpHookMu.RUnlock()
	return hook
}
F
function

notifyHTTPHook

Parameters

hook
func(bool, string, int, time.Duration)
start
bool
url
string
status
int
duration
http/http.go:55-60
func notifyHTTPHook(hook func(bool, string, int, time.Duration), start bool, url string, status int, duration time.Duration)

{
	if hook == nil {
		return
	}
	js.Guard("HTTP observer", func() { hook(start, url, status, duration) })
}
F
function

SetNativeClient

SetNativeClient has no effect in browser builds.

Parameters

http/http.go:63-63
func SetNativeClient(_ *stdhttp.Client)

{}
F
function

FetchJSON

FetchJSON retrieves JSON data from the given URL and decodes it into v.
Results are cached by URL. If a request is already in progress, FetchJSON
returns ErrPending.

Parameters

url
string
v
any

Returns

error
http/http.go:68-118
func FetchJSON(url string, v any) error

{
	ceIface, _ := cache.LoadOrStore(url, &cacheEntry{ready: make(chan struct{})})
	ce := ceIface.(*cacheEntry)

	ce.once.Do(func() {
		go func() {
			hook := currentHTTPHook()
			notifyHTTPHook(hook, true, url, 0, 0)
			start := time.Now()
			js.Fetch(url).Call("then",
				js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
					resp := args[0]
					status := resp.Get("status").Int()
					resp.Call("json").Call("then",
						js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
							obj := args[0]
							jsonStr := js.GlobalJSON().Call("stringify", obj).String()
							ce.data = []byte(jsonStr)
							notifyHTTPHook(hook, false, url, status, time.Since(start))
							close(ce.ready)
							return nil
						}),
						js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
							ce.err = errors.New(args[0].String())
							notifyHTTPHook(hook, false, url, status, time.Since(start))
							close(ce.ready)
							return nil
						}),
					)
					return nil
				}),
				js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
					ce.err = errors.New(args[0].String())
					notifyHTTPHook(hook, false, url, 0, time.Since(start))
					close(ce.ready)
					return nil
				}),
			)
		}()
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return ce.err
		}
		return json.Unmarshal(ce.data, v)
	default:
		return ErrPending
	}
}
F
function

FetchText

FetchText retrieves text data from url. Results are cached by URL.
If a request is already in progress, FetchText returns ErrPending.

Parameters

url
string

Returns

string
error
http/http.go:122-170
func FetchText(url string) (string, error)

{
	ceIface, _ := textCache.LoadOrStore(url, &textEntry{ready: make(chan struct{})})
	ce := ceIface.(*textEntry)

	ce.once.Do(func() {
		go func() {
			hook := currentHTTPHook()
			notifyHTTPHook(hook, true, url, 0, 0)
			start := time.Now()
			js.Fetch(url).Call("then",
				js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
					resp := args[0]
					status := resp.Get("status").Int()
					resp.Call("text").Call("then",
						js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
							ce.text = args[0].String()
							notifyHTTPHook(hook, false, url, status, time.Since(start))
							close(ce.ready)
							return nil
						}),
						js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
							ce.err = errors.New(args[0].String())
							notifyHTTPHook(hook, false, url, status, time.Since(start))
							close(ce.ready)
							return nil
						}),
					)
					return nil
				}),
				js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
					ce.err = errors.New(args[0].String())
					notifyHTTPHook(hook, false, url, 0, time.Since(start))
					close(ce.ready)
					return nil
				}),
			)
		}()
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return "", ce.err
		}
		return ce.text, nil
	default:
		return "", ErrPending
	}
}
F
function

ClearCache

ClearCache removes any cached response for the given URL.

Parameters

url
string
http/http.go:173-176
func ClearCache(url string)

{
	cache.Delete(url)
	textCache.Delete(url)
}
F
function

waitNoPending

Parameters

fn
func() error
http/http_nonwasm_test.go:15-31
func waitNoPending(t *testing.T, fn func() error)

{
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for {
		err := fn()
		if err == nil {
			return
		}
		if err != ErrPending {
			t.Fatalf("unexpected error: %v", err)
		}
		if time.Now().After(deadline) {
			t.Fatalf("timed out waiting for request to complete")
		}
		time.Sleep(5 * time.Millisecond)
	}
}
F
function

waitText

Parameters

fn
func() (string, error)

Returns

string
http/http_nonwasm_test.go:33-49
func waitText(t *testing.T, fn func() (string, error)) string

{
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for {
		s, err := fn()
		if err == nil {
			return s
		}
		if err != ErrPending {
			t.Fatalf("unexpected error: %v", err)
		}
		if time.Now().After(deadline) {
			t.Fatalf("timed out waiting for request to complete")
		}
		time.Sleep(5 * time.Millisecond)
	}
}
F
function

TestFetchBytesRejectsNonHTTPURLs

Parameters

http/http_nonwasm_test.go:51-59
func TestFetchBytesRejectsNonHTTPURLs(t *testing.T)

{
	for _, rawURL := range []string{"file:///tmp/secret", "/relative", "http://127.0.0.1/secret"} {
		if _, _, err := fetchBytes(rawURL); err == nil {
			t.Fatalf("expected %q to be rejected", rawURL)
		} else if strings.HasPrefix(rawURL, "http://127.") && !strings.Contains(err.Error(), "not public") {
			t.Fatalf("unexpected private URL rejection: %v", err)
		}
	}
}
F
function

TestFetchText_CacheAndPending

Parameters

http/http_nonwasm_test.go:61-96
func TestFetchText_CacheAndPending(t *testing.T)

{
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		hits++
		time.Sleep(30 * time.Millisecond)
		w.WriteHeader(200)
		_, _ = w.Write([]byte("hello"))
	}))
	defer srv.Close()
	oldClient := currentNativeClient()
	SetNativeClient(srv.Client())
	t.Cleanup(func() { SetNativeClient(oldClient) })

	ClearCache(srv.URL)
	t.Cleanup(func() { ClearCache(srv.URL) })

	if _, err := FetchText(srv.URL); err != ErrPending {
		t.Fatalf("expected ErrPending, got %v", err)
	}

	got := waitText(t, func() (string, error) { return FetchText(srv.URL) })
	if got != "hello" {
		t.Fatalf("expected 'hello', got %q", got)
	}

	got2, err := FetchText(srv.URL)
	if err != nil {
		t.Fatalf("expected cached success, got %v", err)
	}
	if got2 != "hello" {
		t.Fatalf("expected cached 'hello', got %q", got2)
	}
	if hits != 1 {
		t.Fatalf("expected 1 hit, got %d", hits)
	}
}
F
function

TestFetchJSON_DecodeAndHook

Parameters

http/http_nonwasm_test.go:98-153
func TestFetchJSON_DecodeAndHook(t *testing.T)

{
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		time.Sleep(20 * time.Millisecond)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(200)
		_, _ = w.Write([]byte(`{"ok":true,"n":3}`))
	}))
	defer srv.Close()
	oldClient := currentNativeClient()
	SetNativeClient(srv.Client())
	t.Cleanup(func() { SetNativeClient(oldClient) })

	ClearCache(srv.URL)
	t.Cleanup(func() { ClearCache(srv.URL) })

	var mu sync.Mutex
	var starts, completes int
	var gotStatus int
	RegisterHTTPHook(func(start bool, _ string, status int, d time.Duration) {
		mu.Lock()
		defer mu.Unlock()
		if start {
			starts++
			return
		}
		completes++
		gotStatus = status
		if d <= 0 {
			t.Fatalf("expected duration > 0")
		}
	})
	t.Cleanup(func() { RegisterHTTPHook(nil) })

	var out struct {
		OK bool `json:"ok"`
		N  int  `json:"n"`
	}

	if err := FetchJSON(srv.URL, &out); err != ErrPending {
		t.Fatalf("expected ErrPending, got %v", err)
	}

	waitNoPending(t, func() error { return FetchJSON(srv.URL, &out) })
	if !out.OK || out.N != 3 {
		t.Fatalf("unexpected decoded value: %+v", out)
	}

	mu.Lock()
	defer mu.Unlock()
	if starts != 1 || completes != 1 {
		t.Fatalf("expected 1 start and 1 complete, got %d and %d", starts, completes)
	}
	if gotStatus != 200 {
		t.Fatalf("expected status 200, got %d", gotStatus)
	}
}
F
function

TestClearCache_AllowsRefetch

Parameters

http/http_nonwasm_test.go:155-176
func TestClearCache_AllowsRefetch(t *testing.T)

{
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		hits++
		w.WriteHeader(200)
		_, _ = w.Write([]byte("ok"))
	}))
	defer srv.Close()
	oldClient := currentNativeClient()
	SetNativeClient(srv.Client())
	t.Cleanup(func() { SetNativeClient(oldClient) })

	ClearCache(srv.URL)
	_ = waitText(t, func() (string, error) { return FetchText(srv.URL) })

	ClearCache(srv.URL)
	_ = waitText(t, func() (string, error) { return FetchText(srv.URL) })

	if hits != 2 {
		t.Fatalf("expected 2 hits after clear, got %d", hits)
	}
}
S
struct

cacheEntry

http/http_stub.go:20-25
type cacheEntry struct

Fields

Name Type Description
once sync.Once
data []byte
err error
ready chan struct{}
S
struct

textEntry

http/http_stub.go:27-32
type textEntry struct

Fields

Name Type Description
once sync.Once
text string
err error
ready chan struct{}
F
function

RegisterHTTPHook

RegisterHTTPHook registers fn to receive HTTP request events.

Parameters

fn
func(start bool, url string, status int, duration time.Duration)
http/http_stub.go:45-49
func RegisterHTTPHook(fn func(start bool, url string, status int, duration time.Duration))

{
	httpHookMu.Lock()
	httpHook = fn
	httpHookMu.Unlock()
}
F
function

currentHTTPHook

Returns

func(bool,
string, int, time.Duration)
http/http_stub.go:51-56
func currentHTTPHook() func(bool, string, int, time.Duration)

{
	httpHookMu.RLock()
	hook := httpHook
	httpHookMu.RUnlock()
	return hook
}
F
function

SetNativeClient

SetNativeClient replaces the native HTTP client. Custom clients may reach
private networks and must only receive trusted URLs. Passing nil restores
the default client, which rejects private network addresses.

Parameters

client
http/http_stub.go:61-68
func SetNativeClient(client *stdhttp.Client)

{
	if client == nil {
		client = safehttp.NewClient()
	}
	httpClientMu.Lock()
	httpClient = client
	httpClientMu.Unlock()
}
F
function

currentNativeClient

Returns

http/http_stub.go:70-75
func currentNativeClient() *stdhttp.Client

{
	httpClientMu.RLock()
	client := httpClient
	httpClientMu.RUnlock()
	return client
}
F
function

fetchBytes

Parameters

rawURL
string

Returns

status
int
body
[]byte
err
error
http/http_stub.go:77-99
func fetchBytes(rawURL string) (status int, body []byte, err error)

{
	req, err := safehttp.NewRequest(context.Background(), stdhttp.MethodGet, rawURL)
	if err != nil {
		return 0, nil, err
	}
	resp, err := currentNativeClient().Do(req)
	if err != nil {
		return 0, nil, err
	}

	b, err := io.ReadAll(resp.Body)
	closeErr := resp.Body.Close()
	if err != nil {
		return resp.StatusCode, nil, err
	}
	if closeErr != nil {
		return resp.StatusCode, nil, closeErr
	}
	if resp.StatusCode >= 400 {
		return resp.StatusCode, nil, errors.New(string(b))
	}
	return resp.StatusCode, b, nil
}
F
function

FetchJSON

FetchJSON retrieves JSON data from the given URL and decodes it into v.
Results are cached by URL. If a request is already in progress, FetchJSON
returns ErrPending.

Parameters

url
string
v
any

Returns

error
http/http_stub.go:104-134
func FetchJSON(url string, v any) error

{
	ceIface, _ := cache.LoadOrStore(url, &cacheEntry{ready: make(chan struct{})})
	ce := ceIface.(*cacheEntry)

	ce.once.Do(func() {
		go func() {
			hook := currentHTTPHook()
			if hook != nil {
				hook(true, url, 0, 0)
			}
			start := time.Now()
			status, b, err := fetchBytes(url)
			ce.data = b
			ce.err = err
			if hook != nil {
				hook(false, url, status, time.Since(start))
			}
			close(ce.ready)
		}()
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return ce.err
		}
		return json.Unmarshal(ce.data, v)
	default:
		return ErrPending
	}
}
F
function

FetchText

FetchText retrieves text data from url. Results are cached by URL.
If a request is already in progress, FetchText returns ErrPending.

Parameters

url
string

Returns

string
error
http/http_stub.go:138-168
func FetchText(url string) (string, error)

{
	ceIface, _ := textCache.LoadOrStore(url, &textEntry{ready: make(chan struct{})})
	ce := ceIface.(*textEntry)

	ce.once.Do(func() {
		go func() {
			hook := currentHTTPHook()
			if hook != nil {
				hook(true, url, 0, 0)
			}
			start := time.Now()
			status, b, err := fetchBytes(url)
			ce.text = string(b)
			ce.err = err
			if hook != nil {
				hook(false, url, status, time.Since(start))
			}
			close(ce.ready)
		}()
	})

	select {
	case <-ce.ready:
		if ce.err != nil {
			return "", ce.err
		}
		return ce.text, nil
	default:
		return "", ErrPending
	}
}
F
function

ClearCache

ClearCache removes any cached response for the given URL.

Parameters

url
string
http/http_stub.go:171-174
func ClearCache(url string)

{
	cache.Delete(url)
	textCache.Delete(url)
}
F
function

TestRegisterHTTPHook

Parameters

http/http_test.go:12-29
func TestRegisterHTTPHook(t *testing.T)

{
	var starts, completes int
	RegisterHTTPHook(func(start bool, _ string, _ int, _ time.Duration) {
		if start {
			starts++
		} else {
			completes++
		}
	})
	if httpHook == nil {
		t.Fatal("hook not registered")
	}
	httpHook(true, "u", 0, 0)
	httpHook(false, "u", 200, time.Millisecond)
	if starts != 1 || completes != 1 {
		t.Fatalf("expected 1 start and 1 complete, got %d and %d", starts, completes)
	}
}
F
function

TestHTTPHookPanicIsIsolated

Parameters

http/http_test.go:31-44
func TestHTTPHookPanicIsIsolated(t *testing.T)

{
	previous := js.OnRuntimePanic
	defer func() { js.OnRuntimePanic = previous }()
	recovered := 0
	js.OnRuntimePanic = func(any, string, []byte) { recovered++ }

	notifyHTTPHook(func(bool, string, int, time.Duration) {
		panic("observer")
	}, true, "u", 0, 0)

	if recovered != 1 {
		t.Fatalf("recovered observer panics = %d, want 1", recovered)
	}
}
S
struct

RequestOptions

RequestOptions configures a raw fetch performed by Request.

http/request.go:8-18
type RequestOptions struct

Methods

apply
Method

apply builds the fetch init object for the request.

Returns

func (RequestOptions) apply() js.Value
{
	init := js.Object().New()
	if o.Method != "" {
		init.Set("method", o.Method)
	}
	if len(o.Headers) > 0 {
		h := js.Object().New()
		for k, v := range o.Headers {
			h.Set(k, v)
		}
		init.Set("headers", h)
	}
	if o.BodyValue.Truthy() {
		init.Set("body", o.BodyValue)
	} else if o.Body != "" {
		init.Set("body", o.Body)
	}
	return init
}

Fields

Name Type Description
Method string
Headers map[string]string
Body string
BodyValue js.Value
F
function

Request

Request performs an uncached fetch with a custom method, headers and body and
invokes cb with the HTTP status and the response body text once it resolves.

Unlike FetchJSON/FetchText it does not cache and carries request headers, so
it is the right primitive for authenticated and mutating requests (the caller
supplies Authorization / workspace headers via RequestOptions.Headers). cb is
invoked on the JS event loop; it may be nil.

Parameters

url
string
cb
func(status int, body string)
http/request.go:48-78
func Request(url string, opts RequestOptions, cb func(status int, body string))

{
	status := 0
	var onResp, onText, onErr js.Func
	// Every callback is released on both outcomes: releasing only the one that
	// fired leaks the other two for the lifetime of the page.
	release := func() {
		onResp.Release()
		onText.Release()
		onErr.Release()
	}
	onText = js.SafeFuncOf(func(_ js.Value, a []js.Value) any {
		release()
		if cb != nil {
			cb(status, a[0].String())
		}
		return nil
	})
	onResp = js.SafeFuncOf(func(_ js.Value, a []js.Value) any {
		status = a[0].Get("status").Int()
		a[0].Call("text").Call("then", onText).Call("catch", onErr)
		return nil
	})
	onErr = js.SafeFuncOf(func(_ js.Value, _ []js.Value) any {
		release()
		if cb != nil {
			cb(0, "")
		}
		return nil
	})
	js.Fetch(url, opts.apply()).Call("then", onResp).Call("catch", onErr)
}
F
function

RequestBytes

RequestBytes performs an uncached fetch like Request but delivers the raw
response bytes via arrayBuffer, so binary payloads (images, chunks,
downloads) survive intact; Request’s text decoding would corrupt them.

Parameters

url
string
cb
func(status int, body []byte)
http/request.go:83-114
func RequestBytes(url string, opts RequestOptions, cb func(status int, body []byte))

{
	status := 0
	var onResp, onBuf, onErr js.Func
	release := func() {
		onResp.Release()
		onBuf.Release()
		onErr.Release()
	}
	onBuf = js.SafeFuncOf(func(_ js.Value, a []js.Value) any {
		u8 := js.Uint8Array().New(a[0])
		body := make([]byte, u8.Get("length").Int())
		js.CopyBytesToGo(body, u8)
		release()
		if cb != nil {
			cb(status, body)
		}
		return nil
	})
	onResp = js.SafeFuncOf(func(_ js.Value, a []js.Value) any {
		status = a[0].Get("status").Int()
		a[0].Call("arrayBuffer").Call("then", onBuf).Call("catch", onErr)
		return nil
	})
	onErr = js.SafeFuncOf(func(_ js.Value, _ []js.Value) any {
		release()
		if cb != nil {
			cb(0, nil)
		}
		return nil
	})
	js.Fetch(url, opts.apply()).Call("then", onResp).Call("catch", onErr)
}
F
function

TestRequestOptionsApply

Parameters

http/request_options_test.go:11-27
func TestRequestOptionsApply(t *testing.T)

{
	init := RequestOptions{
		Method:  "POST",
		Headers: map[string]string{"X-Test": "1"},
		Body:    `{"a":1}`,
	}.apply()

	if got := init.Get("method").String(); got != "POST" {
		t.Fatalf("method = %q", got)
	}
	if got := init.Get("headers").Get("X-Test").String(); got != "1" {
		t.Fatalf("header = %q", got)
	}
	if got := init.Get("body").String(); got != `{"a":1}` {
		t.Fatalf("body = %q", got)
	}
}
F
function

TestRequestOptionsBodyValueWins

Parameters

http/request_options_test.go:29-42
func TestRequestOptionsBodyValueWins(t *testing.T)

{
	form := js.FormData().New()
	form.Call("append", "k", "v")

	init := RequestOptions{Method: "POST", Body: "ignored", BodyValue: form}.apply()

	body := init.Get("body")
	if body.Type() == js.TypeString {
		t.Fatalf("BodyValue lost to Body: %q", body.String())
	}
	if got := body.Call("get", "k").String(); got != "v" {
		t.Fatalf("form field = %q", got)
	}
}
F
function

TestRequestOptionsEmptyBodyIsUnset

Parameters

http/request_options_test.go:44-52
func TestRequestOptionsEmptyBodyIsUnset(t *testing.T)

{
	init := RequestOptions{}.apply()
	if init.Get("body").Type() != js.TypeUndefined {
		t.Fatal("empty options set a body")
	}
	if init.Get("method").Type() != js.TypeUndefined {
		t.Fatal("empty options set a method")
	}
}