├── .travis.yml ├── examples ├── get │ └── main.go └── get-json │ └── main.go ├── .gitignore ├── README.md ├── LICENSE ├── example_test.go ├── status_test.go ├── status.go ├── requests_test.go └── requests.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: github.com/pkg/requests 3 | go: 4 | - 1.10.x 5 | - 1.11.x 6 | - tip 7 | 8 | sudo: false 9 | 10 | install: 11 | - go get -t -v ./... 12 | 13 | script: 14 | - go test ./... && go test -race ./... 15 | -------------------------------------------------------------------------------- /examples/get/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/pkg/requests" 8 | ) 9 | 10 | func main() { 11 | var client requests.Client 12 | resp, err := client.Get("https://httpbin.org/get") 13 | if err != nil { 14 | log.Fatalf("%+v", err) 15 | } 16 | fmt.Println(resp.Request.Method, resp.Request.URL, resp.Status.Code) 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # requests 2 | 3 | [![Build Status](https://travis-ci.org/pkg/requests.svg?branch=master)](https://travis-ci.org/pkg/requests) 4 | 5 | HTTP for Gophers. 6 | 7 | ## Roadmap 8 | 9 | requests temporarily uses the net/http package, but plans to move to its own HTTP/1.1 and HTTP/2.0 client. 10 | 11 | * [ ] HTTP/1.1 and HTTP/2.0 support 12 | * [ ] RFC 2068 proxy support 13 | * [ ] SOCKS4/5 proxy support 14 | -------------------------------------------------------------------------------- /examples/get-json/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/pkg/requests" 8 | ) 9 | 10 | func main() { 11 | var client requests.Client 12 | resp, err := client.Get("https://httpbin.org/get") 13 | check(err) 14 | 15 | m := make(map[string]interface{}) 16 | err = resp.JSON(&m) 17 | check(err) 18 | 19 | fmt.Printf("%#v\n", m) 20 | } 21 | 22 | func check(err error) { 23 | if err != nil { 24 | log.Fatalf("%+v", err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Dave Cheney 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package requests_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/pkg/requests" 9 | ) 10 | 11 | func ExampleClient_Get() { 12 | var c requests.Client 13 | 14 | resp, err := c.Get("https://www.example.com") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | fmt.Println(resp.Request.Method, resp.Request.URL, resp.Status.Code) 19 | } 20 | 21 | func ExampleClient_Post() { 22 | var c requests.Client 23 | 24 | body := strings.NewReader("Hello there!") 25 | resp, err := c.Post("https://www.example.com", body, requests.WithHeader("Content-Type", "application/x-www-form-urlencoded")) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | fmt.Println(resp.Request.Method, resp.Request.URL, resp.Status.Code) 30 | } 31 | 32 | func ExampleBody_JSON() { 33 | var c requests.Client 34 | 35 | resp, err := c.Get("https://frinkiac.com/api/search?q=burn+that+seat") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | defer resp.Close() 40 | 41 | if !resp.IsSuccess() { 42 | log.Fatalf("%s: expected 200, got %v", resp.Request.URL, resp.Status) 43 | } 44 | 45 | var results []struct { 46 | Id int `json:"Id"` 47 | Episode string `json:"Episode"` 48 | Timestamp int `json:"Timestamp"` 49 | } 50 | 51 | err = resp.JSON(&results) 52 | fmt.Printf("%#v\n%v", results, err) 53 | } 54 | 55 | var response = requests.Response{ 56 | Headers: []requests.Header{ 57 | {Key: "Server", Values: []string{"nginx/1.2.1"}}, 58 | {Key: "Connection", Values: []string{"keep-alive"}}, 59 | {Key: "Content-Type", Values: []string{"text/html; charset=UTF-8"}}, 60 | }, 61 | } 62 | 63 | func ExampleResponse_Header() { 64 | fmt.Println(response.Header("Server")) 65 | fmt.Println(response.Header("Content-Type")) 66 | 67 | // Output: 68 | // nginx/1.2.1 69 | // text/html; charset=UTF-8 70 | } 71 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import "testing" 4 | 5 | func TestStatusString(t *testing.T) { 6 | tests := []struct { 7 | Status 8 | want string 9 | }{ 10 | {Status{Code: 200, Reason: "OK"}, "200 OK"}, 11 | {Status{Code: 418, Reason: "I'm a teapot"}, "418 I'm a teapot"}, 12 | } 13 | 14 | for _, tt := range tests { 15 | got := tt.Status.String() 16 | if got != tt.want { 17 | t.Errorf("got: %q, want: %q", got, tt.want) 18 | } 19 | } 20 | } 21 | 22 | func TestStatusMethods(t *testing.T) { 23 | tests := []struct { 24 | Status 25 | informational, success, redirect, error, clienterr, servererr bool 26 | }{ 27 | {Status{Code: INFO_CONTINUE}, true, false, false, false, false, false}, 28 | {Status{Code: SUCCESS_OK}, false, true, false, false, false, false}, 29 | {Status{Code: REDIRECTION_MULTIPLE_CHOICES}, false, false, true, false, false, false}, 30 | {Status{Code: CLIENT_ERROR_BAD_REQUEST}, false, false, false, true, true, false}, 31 | {Status{Code: SERVER_ERROR_INTERNAL}, false, false, false, true, false, true}, 32 | } 33 | 34 | for _, tt := range tests { 35 | if info := tt.Status.IsInformational(); info != tt.informational { 36 | t.Errorf("Status(%q).Informational: expected %v, got %v", tt.Status, tt.informational, info) 37 | } 38 | if success := tt.Status.IsSuccess(); success != tt.success { 39 | t.Errorf("Status(%q).Success: expected %v, got %v", tt.Status, tt.success, success) 40 | } 41 | if redirect := tt.Status.IsRedirect(); redirect != tt.redirect { 42 | t.Errorf("Status(%q).Redirect: expected %v, got %v", tt.Status, tt.redirect, redirect) 43 | } 44 | if error := tt.Status.IsError(); error != tt.error { 45 | t.Errorf("Status(%q).IsError: expected %v, got %v", tt.Status, tt.error, error) 46 | } 47 | if error := tt.Status.IsClientError(); error != tt.clienterr { 48 | t.Errorf("Status(%q).IsError: expected %v, got %v", tt.Status, tt.clienterr, error) 49 | } 50 | if error := tt.Status.IsServerError(); error != tt.servererr { 51 | t.Errorf("Status(%q).IsError: expected %v, got %v", tt.Status, tt.servererr, error) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import "fmt" 4 | 5 | const ( 6 | INFO_CONTINUE = 100 7 | INFO_SWITCHING_PROTOCOL = 101 8 | INFO_PROCESSING = 102 9 | 10 | SUCCESS_OK = 200 11 | SUCCESS_CREATED = 201 12 | SUCCESS_ACCEPTED = 202 13 | SUCCESS_NON_AUTHORITATIVE = 203 14 | SUCCESS_NO_CONTENT = 204 15 | SUCCESS_RESET_CONTENT = 205 16 | SUCCESS_PARTIAL_CONTENT = 206 17 | SUCCESS_MULTI_STATUS = 207 18 | 19 | REDIRECTION_MULTIPLE_CHOICES = 300 20 | REDIRECTION_MOVED_PERMANENTLY = 301 21 | REDIRECTION_MOVED_TEMPORARILY = 302 22 | REDIRECTION_SEE_OTHER = 303 23 | REDIRECTION_NOT_MODIFIED = 304 24 | REDIRECTION_USE_PROXY = 305 25 | REDIRECTION_TEMPORARY_REDIRECT = 307 26 | 27 | CLIENT_ERROR_BAD_REQUEST = 400 28 | CLIENT_ERROR_UNAUTHORIZED = 401 29 | CLIENT_ERROR_PAYMENT_REQUIRED = 402 30 | CLIENT_ERROR_FORBIDDEN = 403 31 | CLIENT_ERROR_NOT_FOUND = 404 32 | CLIENT_ERROR_METHOD_NOT_ALLOWED = 405 33 | CLIENT_ERROR_NOT_ACCEPTABLE = 406 34 | CLIENT_ERROR_PROXY_AUTHENTIFICATION_REQUIRED = 407 35 | CLIENT_ERROR_REQUEST_TIMEOUT = 408 36 | CLIENT_ERROR_CONFLICT = 409 37 | CLIENT_ERROR_GONE = 410 38 | CLIENT_ERROR_LENGTH_REQUIRED = 411 39 | CLIENT_ERROR_PRECONDITION_FAILED = 412 40 | CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE = 413 41 | CLIENT_ERROR_REQUEST_URI_TOO_LONG = 414 42 | CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE = 415 43 | CLIENT_ERROR_REQUESTED_RANGE_NOT_SATISFIABLE = 416 44 | CLIENT_ERROR_EXPECTATION_FAILED = 417 45 | CLIENT_ERROR_UNPROCESSABLE_ENTITY = 422 46 | CLIENT_ERROR_LOCKED = 423 47 | CLIENT_ERROR_FAILED_DEPENDENCY = 424 48 | 49 | SERVER_ERROR_INTERNAL = 500 50 | SERVER_ERROR_NOT_IMPLEMENTED = 501 51 | SERVER_ERROR_BAD_GATEWAY = 502 52 | SERVER_ERROR_SERVICE_UNAVAILABLE = 503 53 | SERVER_ERROR_GATEWAY_TIMEOUT = 504 54 | SERVER_ERROR_HTTP_VERSION_NOT_SUPPORTED = 505 55 | SERVER_ERROR_INSUFFICIENT_STORAGE = 507 56 | ) 57 | 58 | // Status is a HTTP reponse status. 59 | type Status struct { 60 | Code int 61 | Reason string 62 | } 63 | 64 | func (s *Status) String() string { return fmt.Sprintf("%d %s", s.Code, s.Reason) } 65 | 66 | func (s Status) IsInformational() bool { return s.Code >= INFO_CONTINUE && s.Code < SUCCESS_OK } 67 | func (s Status) IsSuccess() bool { return s.Code >= SUCCESS_OK && s.Code < REDIRECTION_MULTIPLE_CHOICES } 68 | func (s Status) IsRedirect() bool { 69 | return s.Code >= REDIRECTION_MULTIPLE_CHOICES && s.Code < CLIENT_ERROR_BAD_REQUEST 70 | } 71 | func (s Status) IsError() bool { return s.Code >= CLIENT_ERROR_BAD_REQUEST } 72 | func (s Status) IsClientError() bool { 73 | return s.Code >= CLIENT_ERROR_BAD_REQUEST && s.Code < SERVER_ERROR_INTERNAL 74 | } 75 | func (s Status) IsServerError() bool { return s.Code >= SERVER_ERROR_INTERNAL } 76 | -------------------------------------------------------------------------------- /requests_test.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/url" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestBodyJSON(t *testing.T) { 13 | jsonbody := func(s string) io.ReadCloser { 14 | type rc struct { 15 | io.Reader 16 | io.Closer 17 | } 18 | return rc{ 19 | Reader: strings.NewReader(s), 20 | } 21 | } 22 | 23 | type T struct { 24 | A string `json:"a"` 25 | } 26 | 27 | tests := []struct { 28 | Body 29 | want []T 30 | }{{ 31 | Body: Body{ 32 | ReadCloser: jsonbody(`{"a":"hello"}`), 33 | }, 34 | want: []T{{A: "hello"}}, 35 | }, { 36 | Body: Body{ 37 | ReadCloser: jsonbody(`{"a":"first"}{"a":"second"}`), 38 | }, 39 | want: []T{{A: "first"}, {A: "second"}}, 40 | }} 41 | 42 | for _, tt := range tests { 43 | got := make([]T, len(tt.want)) 44 | for i := range got { 45 | if err := tt.Body.JSON(&got[i]); err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | if !reflect.DeepEqual(got, tt.want) { 50 | t.Errorf("got: %v, want: %v", got, tt.want) 51 | } 52 | } 53 | } 54 | 55 | func TestResponseHeader(t *testing.T) { 56 | header := func(key, val string, vals ...string) Header { 57 | v := []string{val} 58 | return Header{ 59 | Key: key, 60 | Values: append(v, vals...), 61 | } 62 | } 63 | 64 | resp := &Response{ 65 | Headers: []Header{ 66 | header("foo", "bar"), 67 | header("quxx", "frob", "frob"), 68 | }, 69 | } 70 | 71 | tests := []struct { 72 | *Response 73 | key string 74 | want string 75 | }{{ 76 | resp, 77 | "foo", 78 | "bar", 79 | }, { 80 | resp, 81 | "quxx", 82 | "frob,frob", 83 | }, { 84 | resp, 85 | "flimm", 86 | "", 87 | }} 88 | 89 | for i, tc := range tests { 90 | got := tc.Header(tc.key) 91 | if got != tc.want { 92 | t.Errorf("%d: Header(%q): got %q, want %v", i, tc.key, got, tc.want) 93 | } 94 | } 95 | } 96 | 97 | func TestToHeaders(t *testing.T) { 98 | tests := []struct { 99 | Headers []Header 100 | want map[string][]string 101 | }{{ 102 | Headers: []Header{ 103 | {Key: "foo", Values: []string{"bar"}}, 104 | {Key: "cram", Values: []string{"witt", "jannet"}}, 105 | }, 106 | want: map[string][]string{ 107 | "foo": []string{"bar"}, 108 | "cram": []string{"witt", "jannet"}, 109 | }, 110 | }, { 111 | Headers: []Header{}, 112 | want: nil, 113 | }} 114 | 115 | for i, tc := range tests { 116 | got := toHeaders(tc.Headers) 117 | if !reflect.DeepEqual(got, tc.want) { 118 | t.Errorf("%d: %v.toHeaders(): got: %v, want: %v", i, tc.Headers, got, tc.want) 119 | } 120 | } 121 | } 122 | 123 | func TestNewHTTPRequest(t *testing.T) { 124 | tests := []struct { 125 | Request 126 | want http.Request 127 | }{{ 128 | Request{ 129 | Method: "GET", 130 | URL: "https://example.com", 131 | Headers: []Header{ 132 | {Key: "Connection", Values: []string{"close"}}, 133 | {Key: "Upgrade", Values: []string{"h2c"}}, 134 | }, 135 | }, 136 | http.Request{ 137 | Method: "GET", 138 | URL: &url.URL{ 139 | Scheme: "https", 140 | Host: "example.com", 141 | }, 142 | Host: "example.com", 143 | Proto: "HTTP/1.1", 144 | ProtoMajor: 1, 145 | ProtoMinor: 1, 146 | Header: http.Header{ 147 | "Connection": []string{"close"}, 148 | "Upgrade": []string{"h2c"}, 149 | }, 150 | }, 151 | }} 152 | 153 | for i, tc := range tests { 154 | got, err := newHttpRequest(&tc.Request) 155 | if err != nil { 156 | t.Errorf("%d: %v: %v", i, tc.Request, err) 157 | continue 158 | } 159 | 160 | if !reflect.DeepEqual(got, &tc.want) { 161 | t.Errorf("%d: %v: got:\n%+v, want:\n%+v", i, tc.Request, got, &tc.want) 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /requests.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Client is a HTTP Client. 14 | type Client struct { 15 | client *http.Client 16 | } 17 | 18 | // Header is a HTTP header. 19 | type Header struct { 20 | Key string 21 | Values []string 22 | } 23 | 24 | // Get issues a GET to the specified URL. 25 | func (c *Client) Get(url string, options ...func(*Request) error) (*Response, error) { 26 | req := Request{ 27 | Method: "GET", 28 | URL: url, 29 | } 30 | if err := applyOptions(&req, options...); err != nil { 31 | return nil, err 32 | } 33 | return c.do(&req) 34 | } 35 | 36 | // Post issues a POST request to the specified URL. 37 | func (c *Client) Post(url string, body io.Reader, options ...func(*Request) error) (*Response, error) { 38 | req := Request{ 39 | Method: "POST", 40 | URL: url, 41 | Body: body, 42 | } 43 | if err := applyOptions(&req, options...); err != nil { 44 | return nil, err 45 | } 46 | return c.do(&req) 47 | } 48 | 49 | // WithHeader applies the header to the request. 50 | func WithHeader(key, value string) func(*Request) error { 51 | return func(r *Request) error { 52 | r.Headers = append(r.Headers, Header{ 53 | Key: key, 54 | Values: []string{value}, 55 | }) 56 | return nil 57 | } 58 | } 59 | 60 | func (c *Client) do(request *Request) (*Response, error) { 61 | req, err := newHttpRequest(request) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if c.client == nil { 67 | c.client = &*http.DefaultClient 68 | jar, _ := cookiejar.New(new(cookiejar.Options)) 69 | c.client.Jar = jar 70 | c.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 71 | return http.ErrUseLastResponse 72 | } 73 | } 74 | 75 | resp, err := c.client.Do(req) 76 | if err != nil { 77 | return nil, errors.WithStack(err) 78 | } 79 | r := Response{ 80 | Request: request, 81 | Status: Status{ 82 | Code: resp.StatusCode, 83 | Reason: resp.Status[4:], 84 | }, 85 | Headers: headers(resp.Header), 86 | Body: Body{ 87 | ReadCloser: resp.Body, 88 | }, 89 | } 90 | return &r, nil 91 | } 92 | 93 | func applyOptions(req *Request, options ...func(*Request) error) error { 94 | for _, opt := range options { 95 | if err := opt(req); err != nil { 96 | return err 97 | } 98 | } 99 | return nil 100 | } 101 | 102 | // Request is a HTTP request. 103 | type Request struct { 104 | Method string 105 | URL string 106 | Headers []Header 107 | Body io.Reader 108 | } 109 | 110 | // newHttpRequest converts a *requests.Request into a *http.Request 111 | func newHttpRequest(request *Request) (*http.Request, error) { 112 | req, err := http.NewRequest(request.Method, request.URL, request.Body) 113 | if err != nil { 114 | return nil, errors.WithStack(err) 115 | } 116 | req.Header = toHeaders(request.Headers) 117 | return req, nil 118 | } 119 | 120 | // toHeaders convers from Request's Headers slice to http.Request's map[string][]string 121 | func toHeaders(headers []Header) map[string][]string { 122 | if len(headers) == 0 { 123 | return nil 124 | } 125 | 126 | m := make(map[string][]string) 127 | for _, h := range headers { 128 | m[h.Key] = h.Values 129 | } 130 | return m 131 | } 132 | 133 | // Response is a HTTP response. 134 | type Response struct { 135 | *Request 136 | Status 137 | Headers []Header 138 | Body 139 | } 140 | 141 | // Header returns the canonicalised version of a response header as a string 142 | // If there is no key present in the response the empty string is returned. 143 | // If multiple headers are present, they are canonicalised into as single string 144 | // by joining them with a comma. See RFC 2616 § 4.2. 145 | func (r *Response) Header(key string) string { 146 | var vals []string 147 | for _, h := range r.Headers { 148 | 149 | // TODO(dfc) § 4.2 states that not all header values can be combined, but equally those 150 | // that cannot be combined with a comma may not be present more than once in a 151 | // header block. 152 | if h.Key == key { 153 | vals = append(vals, h.Values...) 154 | } 155 | } 156 | return strings.Join(vals, ",") 157 | } 158 | 159 | type Body struct { 160 | io.ReadCloser 161 | 162 | json *json.Decoder 163 | } 164 | 165 | // JSON decodes the next JSON encoded object in the body to v. 166 | func (b *Body) JSON(v interface{}) error { 167 | if b.json == nil { 168 | b.json = json.NewDecoder(b) 169 | } 170 | return b.json.Decode(v) 171 | } 172 | 173 | // return the body as a string, or bytes, or something 174 | 175 | func headers(h map[string][]string) []Header { 176 | headers := make([]Header, 0, len(h)) 177 | for k, v := range h { 178 | headers = append(headers, Header{ 179 | Key: k, 180 | Values: v, 181 | }) 182 | } 183 | return headers 184 | } 185 | --------------------------------------------------------------------------------