http
packageAPI reference for the http
package.
Imports
(12)cacheEntry
cacheEntry holds the result of a fetch operation.
type cacheEntry struct
Fields
| Name | Type | Description |
|---|---|---|
| once | sync.Once | |
| data | []byte | |
| err | error | |
| ready | chan struct{} |
textEntry
type textEntry struct
Fields
| Name | Type | Description |
|---|---|---|
| once | sync.Once | |
| text | string | |
| err | error | |
| ready | chan struct{} |
RegisterHTTPHook
RegisterHTTPHook registers fn to receive HTTP request events.
Parameters
func RegisterHTTPHook(fn func(start bool, url string, status int, duration time.Duration))
{
httpHookMu.Lock()
httpHook = fn
httpHookMu.Unlock()
}
currentHTTPHook
Returns
func currentHTTPHook() func(bool, string, int, time.Duration)
{
httpHookMu.RLock()
hook := httpHook
httpHookMu.RUnlock()
return hook
}
notifyHTTPHook
Parameters
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) })
}
SetNativeClient
SetNativeClient has no effect in browser builds.
Parameters
func SetNativeClient(_ *stdhttp.Client)
{}
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
Returns
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
}
}
FetchText
FetchText retrieves text data from url. Results are cached by URL.
If a request is already in progress, FetchText returns ErrPending.
Parameters
Returns
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
}
}
ClearCache
ClearCache removes any cached response for the given URL.
Parameters
func ClearCache(url string)
{
cache.Delete(url)
textCache.Delete(url)
}
waitNoPending
Parameters
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)
}
}
waitText
Parameters
Returns
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)
}
}
TestFetchBytesRejectsNonHTTPURLs
Parameters
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)
}
}
}
TestFetchText_CacheAndPending
Parameters
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)
}
}
TestFetchJSON_DecodeAndHook
Parameters
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)
}
}
TestClearCache_AllowsRefetch
Parameters
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)
}
}
cacheEntry
type cacheEntry struct
Fields
| Name | Type | Description |
|---|---|---|
| once | sync.Once | |
| data | []byte | |
| err | error | |
| ready | chan struct{} |
textEntry
type textEntry struct
Fields
| Name | Type | Description |
|---|---|---|
| once | sync.Once | |
| text | string | |
| err | error | |
| ready | chan struct{} |
RegisterHTTPHook
RegisterHTTPHook registers fn to receive HTTP request events.
Parameters
func RegisterHTTPHook(fn func(start bool, url string, status int, duration time.Duration))
{
httpHookMu.Lock()
httpHook = fn
httpHookMu.Unlock()
}
currentHTTPHook
Returns
func currentHTTPHook() func(bool, string, int, time.Duration)
{
httpHookMu.RLock()
hook := httpHook
httpHookMu.RUnlock()
return hook
}
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
func SetNativeClient(client *stdhttp.Client)
{
if client == nil {
client = safehttp.NewClient()
}
httpClientMu.Lock()
httpClient = client
httpClientMu.Unlock()
}
currentNativeClient
Returns
func currentNativeClient() *stdhttp.Client
{
httpClientMu.RLock()
client := httpClient
httpClientMu.RUnlock()
return client
}
fetchBytes
Parameters
Returns
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
}
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
Returns
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
}
}
FetchText
FetchText retrieves text data from url. Results are cached by URL.
If a request is already in progress, FetchText returns ErrPending.
Parameters
Returns
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
}
}
ClearCache
ClearCache removes any cached response for the given URL.
Parameters
func ClearCache(url string)
{
cache.Delete(url)
textCache.Delete(url)
}
TestRegisterHTTPHook
Parameters
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)
}
}
TestHTTPHookPanicIsIsolated
Parameters
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)
}
}
RequestOptions
RequestOptions configures a raw fetch performed by Request.
type RequestOptions struct
Methods
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 |
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
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)
}
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
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)
}
TestRequestOptionsApply
Parameters
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)
}
}
TestRequestOptionsBodyValueWins
Parameters
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)
}
}
TestRequestOptionsEmptyBodyIsUnset
Parameters
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")
}
}