├── .gitignore ├── LICENSE ├── README.md ├── doc.go ├── enums.go ├── fetch.go ├── go.mod └── header.go /.gitignore: -------------------------------------------------------------------------------- 1 | server 2 | client -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marwan Sulaiman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WASM-FETCH 2 | [![GoDoc](https://godoc.org/marwan.io/wasm-fetch?status.svg)](https://godoc.org/marwan.io/wasm-fetch) 3 | 4 | A go-wasm library that wraps the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 5 | 6 | ### Install 7 | `go get marwan.io/wasm-fetch` 8 | 9 | ### Motivation 10 | Importing net/http adds ~4 MBs to your wasm binary. If that's an issue for you, you can use this 11 | library to make fetch calls. 12 | 13 | 14 | ### Example 15 | 16 | ```golang 17 | package main 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "marwan.io/wasm-fetch" 24 | ) 25 | 26 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 27 | defer cancel() 28 | resp, err := fetch.Fetch("/some/api/call", &fetch.Opts{ 29 | Body: strings.NewReader(`{"one": "two"}`), 30 | Method: fetch.MethodPost, 31 | Signal: ctx, 32 | }) 33 | // use response... 34 | ``` 35 | 36 | 37 | ### Status 38 | GO-WASM is currently experimental and therefore this package is experimental as well, things can break unexpectedly. -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package fetch is a Web Assembly fetch wrapper that avoids importing net/http. 2 | /* 3 | package main 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "marwan.io/wasm-fetch" 10 | ) 11 | 12 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 13 | defer cancel() 14 | resp, err := fetch.Fetch("/some/api/call", &fetch.Opts{ 15 | Body: strings.NewReader(`{"one": "two"}`), 16 | Method: fetch.MethodPost, 17 | Signal: ctx, 18 | }) 19 | */ 20 | package fetch // import "marwan.io/wasm-fetch" 21 | -------------------------------------------------------------------------------- /enums.go: -------------------------------------------------------------------------------- 1 | package fetch 2 | 3 | // cache enums 4 | const ( 5 | CacheDefault = "default" 6 | CacheNoStore = "no-store" 7 | CacheReload = "reload" 8 | CacheNone = "no-cache" 9 | CacheForce = "force-cache" 10 | CacheOnlyIfCached = "only-if-cached" 11 | ) 12 | 13 | // credentials enums 14 | const ( 15 | CredentialsOmit = "omit" 16 | CredentialsSameOrigin = "same-origin" 17 | CredentialsInclude = "include" 18 | ) 19 | 20 | // Common HTTP methods. 21 | // 22 | // Unless otherwise noted, these are defined in RFC 7231 section 4.3. 23 | const ( 24 | MethodGet = "GET" 25 | MethodHead = "HEAD" 26 | MethodPost = "POST" 27 | MethodPut = "PUT" 28 | MethodPatch = "PATCH" // RFC 5789 29 | MethodDelete = "DELETE" 30 | MethodConnect = "CONNECT" 31 | MethodOptions = "OPTIONS" 32 | MethodTrace = "TRACE" 33 | ) 34 | 35 | // Mode enums 36 | const ( 37 | ModeSameOrigin = "same-origin" 38 | ModeNoCORS = "no-cors" 39 | ModeCORS = "cors" 40 | ModeNavigate = "navigate" 41 | ) 42 | 43 | // Redirect enums 44 | const ( 45 | RedirectFollow = "follow" 46 | RedirectError = "error" 47 | RedirectManual = "manual" 48 | ) 49 | 50 | // Referrer enums 51 | const ( 52 | ReferrerNone = "no-referrer" 53 | ReferrerClient = "client" 54 | ) 55 | 56 | // ReferrerPolicy enums 57 | const ( 58 | ReferrerPolicyNone = "no-referrer" 59 | ReferrerPolicyNoDowngrade = "no-referrer-when-downgrade" 60 | ReferrerPolicyOrigin = "origin" 61 | ReferrerPolicyCrossOrigin = "origin-when-cross-origin" 62 | ReferrerPolicyUnsafeURL = "unsafe-url" 63 | ) 64 | -------------------------------------------------------------------------------- /fetch.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package fetch 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "errors" 9 | "io" 10 | "syscall/js" 11 | ) 12 | 13 | // Opts are the options you can pass to the fetch call. 14 | type Opts struct { 15 | // Method is the http verb (constants are copied from net/http to avoid import) 16 | Method string 17 | 18 | // Headers is a map of http headers to send. 19 | Headers map[string]string 20 | 21 | // Body is the body request 22 | Body io.Reader 23 | 24 | // Mode docs https://developer.mozilla.org/en-US/docs/Web/API/Request/mode 25 | Mode string 26 | 27 | // Credentials docs https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials 28 | Credentials string 29 | 30 | // Cache docs https://developer.mozilla.org/en-US/docs/Web/API/Request/cache 31 | Cache string 32 | 33 | // Redirect docs https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch 34 | Redirect string 35 | 36 | // Referrer docs https://developer.mozilla.org/en-US/docs/Web/API/Request/referrer 37 | Referrer string 38 | 39 | // ReferrerPolicy docs https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch 40 | ReferrerPolicy string 41 | 42 | // Integrity docs https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity 43 | Integrity string 44 | 45 | // KeepAlive docs https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch 46 | KeepAlive *bool 47 | 48 | // Signal docs https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal 49 | Signal context.Context 50 | } 51 | 52 | // Response is the response that retursn from the fetch promise. 53 | type Response struct { 54 | Headers Header 55 | OK bool 56 | Redirected bool 57 | Status int 58 | StatusText string 59 | Type string 60 | URL string 61 | Body []byte 62 | BodyUsed bool 63 | } 64 | 65 | // Fetch uses the JS Fetch API to make requests 66 | // over WASM. 67 | func Fetch(url string, opts *Opts) (*Response, error) { 68 | optsMap, err := mapOpts(opts) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | type fetchResponse struct { 74 | r *Response 75 | b js.Value 76 | e error 77 | } 78 | ch := make(chan *fetchResponse) 79 | done := make(chan struct{}, 1) 80 | if opts.Signal != nil { 81 | controller := js.Global().Get("AbortController").New() 82 | signal := controller.Get("signal") 83 | optsMap["signal"] = signal 84 | go func() { 85 | select { 86 | case <-opts.Signal.Done(): 87 | controller.Call("abort") 88 | case <-done: 89 | } 90 | }() 91 | } 92 | 93 | success := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 94 | r := new(Response) 95 | resp := args[0] 96 | headersIt := resp.Get("headers").Call("entries") 97 | headers := Header{} 98 | for { 99 | n := headersIt.Call("next") 100 | if n.Get("done").Bool() { 101 | break 102 | } 103 | pair := n.Get("value") 104 | key, value := pair.Index(0).String(), pair.Index(1).String() 105 | headers.Add(key, value) 106 | } 107 | r.Headers = headers 108 | r.OK = resp.Get("ok").Bool() 109 | r.Redirected = resp.Get("redirected").Bool() 110 | r.Status = resp.Get("status").Int() 111 | r.StatusText = resp.Get("statusText").String() 112 | r.Type = resp.Get("type").String() 113 | r.URL = resp.Get("url").String() 114 | r.BodyUsed = resp.Get("bodyUsed").Bool() 115 | 116 | ch <- &fetchResponse{r: r, b: resp} 117 | return nil 118 | }) 119 | defer success.Release() 120 | 121 | failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 122 | msg := args[0].Get("message").String() 123 | done <- struct{}{} 124 | ch <- &fetchResponse{e: errors.New(msg)} 125 | return nil 126 | }) 127 | defer failure.Release() 128 | 129 | go js.Global().Call("fetch", url, optsMap).Call("then", success).Call("catch", failure) 130 | 131 | r := <-ch 132 | if r.e != nil { 133 | return nil, r.e 134 | } 135 | 136 | successBody := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 137 | // Wrap the input ArrayBuffer with a Uint8Array 138 | uint8arrayWrapper := js.Global().Get("Uint8Array").New(args[0]) 139 | r.r.Body = make([]byte, uint8arrayWrapper.Get("byteLength").Int()) 140 | js.CopyBytesToGo(r.r.Body, uint8arrayWrapper) 141 | ch <- r 142 | return nil 143 | }) 144 | defer successBody.Release() 145 | 146 | failureBody := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 147 | // Assumes it's a TypeError. See 148 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError 149 | // for more information on this type. 150 | // See https://fetch.spec.whatwg.org/#concept-body-consume-body for error causes. 151 | msg := args[0].Get("message").String() 152 | ch <- &fetchResponse{e: errors.New(msg)} 153 | return nil 154 | }) 155 | defer failureBody.Release() 156 | 157 | go r.b.Call("arrayBuffer").Call("then", successBody, failureBody) 158 | 159 | r = <-ch 160 | return r.r, r.e 161 | } 162 | 163 | // oof. 164 | func mapOpts(opts *Opts) (map[string]interface{}, error) { 165 | mp := map[string]interface{}{} 166 | 167 | if opts.Method != "" { 168 | mp["method"] = opts.Method 169 | } 170 | if opts.Headers != nil { 171 | mp["headers"] = mapHeaders(opts.Headers) 172 | } 173 | if opts.Mode != "" { 174 | mp["mode"] = opts.Mode 175 | } 176 | if opts.Credentials != "" { 177 | mp["credentials"] = opts.Credentials 178 | } 179 | if opts.Cache != "" { 180 | mp["cache"] = opts.Cache 181 | } 182 | if opts.Redirect != "" { 183 | mp["redirect"] = opts.Redirect 184 | } 185 | if opts.Referrer != "" { 186 | mp["referrer"] = opts.Referrer 187 | } 188 | if opts.ReferrerPolicy != "" { 189 | mp["referrerPolicy"] = opts.ReferrerPolicy 190 | } 191 | if opts.Integrity != "" { 192 | mp["integrity"] = opts.Integrity 193 | } 194 | if opts.KeepAlive != nil { 195 | mp["keepalive"] = *opts.KeepAlive 196 | } 197 | 198 | if opts.Body != nil { 199 | var bts []byte 200 | var err error 201 | 202 | switch v := opts.Body.(type) { 203 | case *bytes.Buffer: 204 | bts = v.Bytes() 205 | default: 206 | bts, err = io.ReadAll(opts.Body) 207 | } 208 | 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | jsbody := js.Global().Get("Uint8Array").New(len(bts)) 214 | js.CopyBytesToJS(jsbody, bts) 215 | 216 | mp["body"] = jsbody 217 | } 218 | 219 | return mp, nil 220 | } 221 | 222 | func mapHeaders(mp map[string]string) map[string]interface{} { 223 | newMap := map[string]interface{}{} 224 | for k, v := range mp { 225 | newMap[k] = v 226 | } 227 | return newMap 228 | } 229 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module marwan.io/wasm-fetch 2 | 3 | go 1.12 -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package fetch 2 | 3 | import ( 4 | "io" 5 | "net/textproto" 6 | "sort" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // A Header represents the key-value pairs in an HTTP header. 12 | type Header map[string][]string 13 | 14 | // Add adds the key, value pair to the header. 15 | // It appends to any existing values associated with key. 16 | func (h Header) Add(key, value string) { 17 | textproto.MIMEHeader(h).Add(key, value) 18 | } 19 | 20 | // Set sets the header entries associated with key to 21 | // the single element value. It replaces any existing 22 | // values associated with key. 23 | func (h Header) Set(key, value string) { 24 | textproto.MIMEHeader(h).Set(key, value) 25 | } 26 | 27 | // Get gets the first value associated with the given key. 28 | // It is case insensitive; textproto.CanonicalMIMEHeaderKey is used 29 | // to canonicalize the provided key. 30 | // If there are no values associated with the key, Get returns "". 31 | // To access multiple values of a key, or to use non-canonical keys, 32 | // access the map directly. 33 | func (h Header) Get(key string) string { 34 | return textproto.MIMEHeader(h).Get(key) 35 | } 36 | 37 | // get is like Get, but key must already be in CanonicalHeaderKey form. 38 | func (h Header) get(key string) string { 39 | if v := h[key]; len(v) > 0 { 40 | return v[0] 41 | } 42 | return "" 43 | } 44 | 45 | // Del deletes the values associated with key. 46 | func (h Header) Del(key string) { 47 | textproto.MIMEHeader(h).Del(key) 48 | } 49 | 50 | // Write writes a header in wire format. 51 | func (h Header) Write(w io.Writer) error { 52 | return h.write(w) 53 | } 54 | 55 | func (h Header) write(w io.Writer) error { 56 | return h.writeSubset(w, nil) 57 | } 58 | 59 | func (h Header) clone() Header { 60 | h2 := make(Header, len(h)) 61 | for k, vv := range h { 62 | vv2 := make([]string, len(vv)) 63 | copy(vv2, vv) 64 | h2[k] = vv2 65 | } 66 | return h2 67 | } 68 | 69 | var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ") 70 | 71 | type writeStringer interface { 72 | WriteString(string) (int, error) 73 | } 74 | 75 | // stringWriter implements WriteString on a Writer. 76 | type stringWriter struct { 77 | w io.Writer 78 | } 79 | 80 | func (w stringWriter) WriteString(s string) (n int, err error) { 81 | return w.w.Write([]byte(s)) 82 | } 83 | 84 | type keyValues struct { 85 | key string 86 | values []string 87 | } 88 | 89 | // A headerSorter implements sort.Interface by sorting a []keyValues 90 | // by key. It's used as a pointer, so it can fit in a sort.Interface 91 | // interface value without allocation. 92 | type headerSorter struct { 93 | kvs []keyValues 94 | } 95 | 96 | func (s *headerSorter) Len() int { return len(s.kvs) } 97 | func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] } 98 | func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].key < s.kvs[j].key } 99 | 100 | var headerSorterPool = sync.Pool{ 101 | New: func() interface{} { return new(headerSorter) }, 102 | } 103 | 104 | // sortedKeyValues returns h's keys sorted in the returned kvs 105 | // slice. The headerSorter used to sort is also returned, for possible 106 | // return to headerSorterCache. 107 | func (h Header) sortedKeyValues(exclude map[string]bool) (kvs []keyValues, hs *headerSorter) { 108 | hs = headerSorterPool.Get().(*headerSorter) 109 | if cap(hs.kvs) < len(h) { 110 | hs.kvs = make([]keyValues, 0, len(h)) 111 | } 112 | kvs = hs.kvs[:0] 113 | for k, vv := range h { 114 | if !exclude[k] { 115 | kvs = append(kvs, keyValues{k, vv}) 116 | } 117 | } 118 | hs.kvs = kvs 119 | sort.Sort(hs) 120 | return kvs, hs 121 | } 122 | 123 | // WriteSubset writes a header in wire format. 124 | // If exclude is not nil, keys where exclude[key] == true are not written. 125 | func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error { 126 | return h.writeSubset(w, exclude) 127 | } 128 | 129 | func (h Header) writeSubset(w io.Writer, exclude map[string]bool) error { 130 | ws, ok := w.(writeStringer) 131 | if !ok { 132 | ws = stringWriter{w} 133 | } 134 | kvs, sorter := h.sortedKeyValues(exclude) 135 | for _, kv := range kvs { 136 | for _, v := range kv.values { 137 | v = headerNewlineToSpace.Replace(v) 138 | v = textproto.TrimString(v) 139 | for _, s := range []string{kv.key, ": ", v, "\r\n"} { 140 | if _, err := ws.WriteString(s); err != nil { 141 | headerSorterPool.Put(sorter) 142 | return err 143 | } 144 | } 145 | } 146 | } 147 | headerSorterPool.Put(sorter) 148 | return nil 149 | } 150 | 151 | // CanonicalHeaderKey returns the canonical format of the 152 | // header key s. The canonicalization converts the first 153 | // letter and any letter following a hyphen to upper case; 154 | // the rest are converted to lowercase. For example, the 155 | // canonical key for "accept-encoding" is "Accept-Encoding". 156 | // If s contains a space or invalid header field bytes, it is 157 | // returned without modifications. 158 | func CanonicalHeaderKey(s string) string { return textproto.CanonicalMIMEHeaderKey(s) } 159 | --------------------------------------------------------------------------------