pax_global_header 0000666 0000000 0000000 00000000064 13654526166 0014530 g ustar 00root root 0000000 0000000 52 comment=895b231a883c043bfff93926c5228930ea253c8e
httprequest-1.2.1/ 0000775 0000000 0000000 00000000000 13654526166 0014121 5 ustar 00root root 0000000 0000000 httprequest-1.2.1/.travis.yml 0000664 0000000 0000000 00000000156 13654526166 0016234 0 ustar 00root root 0000000 0000000 language: go
go_import_path: "gopkg.in/httprequest.v1"
go:
- "1.11.x"
script: GO111MODULE=on go test ./...
httprequest-1.2.1/LICENSE 0000664 0000000 0000000 00000021053 13654526166 0015127 0 ustar 00root root 0000000 0000000 This software is licensed under the LGPLv3, included below.
As a special exception to the GNU Lesser General Public License version 3
("LGPL3"), the copyright holders of this Library give you permission to
convey to a third party a Combined Work that links statically or dynamically
to this Library without providing any Minimal Corresponding Source or
Minimal Application Code as set out in 4d or providing the installation
information set out in section 4e, provided that you comply with the other
provisions of LGPL3 and provided that you meet, for the Application the
terms and conditions of the license(s) which apply to the Application.
Except as stated in this special exception, the provisions of LGPL3 will
continue to comply in full to this Library. If you modify this Library, you
may apply this exception to your version of this Library, but you are not
obliged to do so. If you do not wish to do so, delete this exception
statement from your version. This exception does not (and cannot) modify any
license terms which apply to the Application, with which you must still
comply.
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
httprequest-1.2.1/README.md 0000664 0000000 0000000 00000052417 13654526166 0015411 0 ustar 00root root 0000000 0000000 # httprequest
--
import "gopkg.in/httprequest.v1"
Package httprequest provides functionality for marshaling unmarshaling HTTP
request parameters into a struct type. It also provides a way to define methods
as HTTP routes using the same approach.
It requires at least Go 1.7, and Go 1.9 is required if the importing program
also uses golang.org/x/net/context.
## Usage
```go
const (
CodeBadRequest = "bad request"
CodeUnauthorized = "unauthorized"
CodeForbidden = "forbidden"
CodeNotFound = "not found"
)
```
These constants are recognized by DefaultErrorMapper as mapping to the similarly
named HTTP status codes.
```go
var (
ErrUnmarshal = errgo.New("httprequest unmarshal error")
ErrBadUnmarshalType = errgo.New("httprequest bad unmarshal type")
)
```
```go
var DefaultErrorMapper = defaultErrorMapper
```
DefaultErrorMapper is used by Server when ErrorMapper is nil. It maps all errors
to RemoteError instances; if an error implements the ErrorCoder interface, the
Code field will be set accordingly; some codes will map to specific HTTP status
codes (for example, if ErrorCode returns CodeBadRequest, the resulting HTTP
status will be http.StatusBadRequest).
```go
var DefaultErrorUnmarshaler = ErrorUnmarshaler(new(RemoteError))
```
DefaultErrorUnmarshaler is the default error unmarshaler used by Client.
#### func AddHandlers
```go
func AddHandlers(r *httprouter.Router, hs []Handler)
```
AddHandlers adds all the handlers in the given slice to r.
#### func ErrorUnmarshaler
```go
func ErrorUnmarshaler(template error) func(*http.Response) error
```
ErrorUnmarshaler returns a function which will unmarshal error responses into
new values of the same type as template. The argument must be a pointer. A new
instance of it is created every time the returned function is called.
If the error cannot by unmarshaled, the function will return an
*HTTPResponseError holding the response from the request.
#### func Marshal
```go
func Marshal(baseURL, method string, x interface{}) (*http.Request, error)
```
Marshal is the counterpart of Unmarshal. It takes information from x, which must
be a pointer to a struct, and returns an HTTP request using the given method
that holds all of the information.
The Body field in the returned request will always be of type BytesReaderCloser.
If x implements the HeaderSetter interface, its SetHeader method will be called
to add additional headers to the HTTP request after it has been marshaled. If x
is pointer to a CustomHeader object then Marshal will use its Body member to
create the HTTP request.
The HTTP request will use the given method. Named fields in the given baseURL
will be filled out from "path"-tagged fields in x to form the URL path in the
returned request. These are specified as for httprouter.
If a field in baseURL is a suffix of the form "*var" (a trailing wildcard
element that holds the rest of the path), the marshaled string must begin with a
"/". This matches the httprouter convention that it always returns such fields
with a "/" prefix.
If a field is of type string or []string, the value of the field will be used
directly; otherwise if implements encoding.TextMarshaler, that will be used to
marshal the field, otherwise fmt.Sprint will be used.
An "omitempty" attribute on a form or header field specifies that if the form or
header value is zero, the form or header entry will be omitted. If the field is
a nil pointer, it will be omitted; otherwise if the field type implements
IsZeroer, that method will be used to determine whether the value is zero,
otherwise if the value is comparable, it will be compared with the zero value
for its type, otherwise the value will never be omitted. One notable
implementation of IsZeroer is time.Time.
An "inbody" attribute on a form field specifies that the field will be marshaled
as part of an application/x-www-form-urlencoded body. Note that the field may
still be unmarshaled from either a URL query parameter or a form-encoded body.
For example, this code:
type UserDetails struct {
Age int
}
type Test struct {
Username string `httprequest:"user,path"`
ContextId int64 `httprequest:"context,form"`
Extra string `httprequest:"context,form,omitempty"`
Details UserDetails `httprequest:",body"`
}
req, err := Marshal("http://example.com/users/:user/details", "GET", &Test{
Username: "bob",
ContextId: 1234,
Details: UserDetails{
Age: 36,
}
})
if err != nil {
...
}
will produce an HTTP request req with a URL of
http://example.com/users/bob/details?context=1234 and a JSON-encoded body
holding `{"Age":36}`.
It is an error if there is a field specified in the URL that is not found in x.
#### func ToHTTP
```go
func ToHTTP(h httprouter.Handle) http.Handler
```
ToHTTP converts an httprouter.Handle into an http.Handler. It will pass no path
variables to h.
#### func Unmarshal
```go
func Unmarshal(p Params, x interface{}) error
```
Unmarshal takes values from given parameters and fills out fields in x, which
must be a pointer to a struct.
Tags on the struct's fields determine where each field is filled in from.
Similar to encoding/json and other encoding packages, the tag holds a
comma-separated list. The first item in the list is an alternative name for the
field (the field name itself will be used if this is empty). The next item
specifies where the field is filled in from. It may be:
"path" - the field is taken from a parameter in p.PathVar
with a matching field name.
"form" - the field is taken from the given name in p.Request.Form
(note that this covers both URL query parameters and
POST form parameters).
"header" - the field is taken from the given name in
p.Request.Header.
"body" - the field is filled in by parsing the request body
as JSON.
For path and form parameters, the field will be filled out from the field in
p.PathVar or p.Form using one of the following methods (in descending order of
preference):
- if the type is string, it will be set from the first value.
- if the type is []string, it will be filled out using all values for that field
(allowed only for form)
- if the type implements encoding.TextUnmarshaler, its UnmarshalText method will
be used
- otherwise fmt.Sscan will be used to set the value.
When the unmarshaling fails, Unmarshal returns an error with an ErrUnmarshal
cause. If the type of x is inappropriate, it returns an error with an
ErrBadUnmarshalType cause.
#### func UnmarshalJSONResponse
```go
func UnmarshalJSONResponse(resp *http.Response, x interface{}) error
```
UnmarshalJSONResponse unmarshals the given HTTP response into x, which should be
a pointer to the result to be unmarshaled into.
If the response cannot be unmarshaled, an error of type *DecodeResponseError
will be returned.
#### func WriteJSON
```go
func WriteJSON(w http.ResponseWriter, code int, val interface{}) error
```
WriteJSON writes the given value to the ResponseWriter and sets the HTTP status
to the given code.
If val implements the HeaderSetter interface, the SetHeader method will be
called to add additional headers to the HTTP response. It is called after the
Content-Type header has been added, so can be used to override the content type
if required.
#### type BytesReaderCloser
```go
type BytesReaderCloser struct {
*bytes.Reader
}
```
BytesReaderCloser is a bytes.Reader which implements io.Closer with a no-op
Close method.
#### func (BytesReaderCloser) Close
```go
func (BytesReaderCloser) Close() error
```
Close implements io.Closer.Close.
#### type Client
```go
type Client struct {
// BaseURL holds the base URL to use when making
// HTTP requests.
BaseURL string
// Doer holds a value that will be used to actually
// make the HTTP request. If it is nil, http.DefaultClient
// will be used instead. If Doer implements DoerWithContext,
// DoWithContext will be used instead.
Doer Doer
// If a request returns an HTTP response that signifies an
// error, UnmarshalError is used to unmarshal the response into
// an appropriate error. See ErrorUnmarshaler for a convenient
// way to create an UnmarshalError function for a given type. If
// this is nil, DefaultErrorUnmarshaler will be used.
UnmarshalError func(resp *http.Response) error
}
```
Client represents a client that can invoke httprequest endpoints.
#### func (*Client) Call
```go
func (c *Client) Call(ctx context.Context, params, resp interface{}) error
```
Call invokes the endpoint implied by the given params, which should be of the
form accepted by the ArgT argument to a function passed to Handle, and
unmarshals the response into the given response parameter, which should be a
pointer to the response value.
If params implements the HeaderSetter interface, its SetHeader method will be
called to add additional headers to the HTTP request.
If resp is nil, the response will be ignored if the request was successful.
If resp is of type **http.Response, instead of unmarshaling into it, its element
will be set to the returned HTTP response directly and the caller is responsible
for closing its Body field.
Any error that c.UnmarshalError or c.Doer returns will not have its cause
masked.
If the request returns a response with a status code signifying success, but the
response could not be unmarshaled, a *DecodeResponseError will be returned
holding the response. Note that if the request returns an error status code, the
Client.UnmarshalError function is responsible for doing this if desired (the
default error unmarshal functions do).
#### func (*Client) CallURL
```go
func (c *Client) CallURL(ctx context.Context, url string, params, resp interface{}) error
```
CallURL is like Call except that the given URL is used instead of c.BaseURL.
#### func (*Client) Do
```go
func (c *Client) Do(ctx context.Context, req *http.Request, resp interface{}) error
```
Do sends the given request and unmarshals its JSON result into resp, which
should be a pointer to the response value. If an error status is returned, the
error will be unmarshaled as in Client.Call.
If resp is nil, the response will be ignored if the response was successful.
If resp is of type **http.Response, instead of unmarshaling into it, its element
will be set to the returned HTTP response directly and the caller is responsible
for closing its Body field.
Any error that c.UnmarshalError or c.Doer returns will not have its cause
masked.
If req.URL does not have a host part it will be treated as relative to
c.BaseURL. req.URL will be updated to the actual URL used.
If the response cannot by unmarshaled, a *DecodeResponseError will be returned
holding the response from the request. the entire response body.
#### func (*Client) Get
```go
func (c *Client) Get(ctx context.Context, url string, resp interface{}) error
```
Get is a convenience method that uses c.Do to issue a GET request to the given
URL. If the given URL does not have a host part then it will be treated as
relative to c.BaseURL.
#### type CustomHeader
```go
type CustomHeader struct {
// Body holds the JSON-marshaled body of the response.
Body interface{}
// SetHeaderFunc holds a function that will be called
// to set any custom headers on the response.
SetHeaderFunc func(http.Header)
}
```
CustomHeader is a type that allows a JSON value to set custom HTTP headers
associated with the HTTP response.
#### func (CustomHeader) MarshalJSON
```go
func (h CustomHeader) MarshalJSON() ([]byte, error)
```
MarshalJSON implements json.Marshaler by marshaling h.Body.
#### func (CustomHeader) SetHeader
```go
func (h CustomHeader) SetHeader(header http.Header)
```
SetHeader implements HeaderSetter by calling h.SetHeaderFunc.
#### type DecodeRequestError
```go
type DecodeRequestError struct {
// Request holds the problematic HTTP request.
// The body of this does not need to be closed
// and may be truncated if the response is large.
Request *http.Request
// DecodeError holds the error that was encountered
// when decoding.
DecodeError error
}
```
DecodeRequestError represents an error when an HTTP request could not be
decoded.
#### func (*DecodeRequestError) Error
```go
func (e *DecodeRequestError) Error() string
```
#### type DecodeResponseError
```go
type DecodeResponseError struct {
// Response holds the problematic HTTP response.
// The body of this does not need to be closed
// and may be truncated if the response is large.
Response *http.Response
// DecodeError holds the error that was encountered
// when decoding.
DecodeError error
}
```
DecodeResponseError represents an error when an HTTP response could not be
decoded.
#### func (*DecodeResponseError) Error
```go
func (e *DecodeResponseError) Error() string
```
#### type Doer
```go
type Doer interface {
Do(req *http.Request) (*http.Response, error)
}
```
Doer is implemented by HTTP client packages to make an HTTP request. It is
notably implemented by http.Client and httpbakery.Client.
#### type DoerWithContext
```go
type DoerWithContext interface {
DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error)
}
```
DoerWithContext is implemented by HTTP clients that can use a context with the
HTTP request.
#### type ErrorCoder
```go
type ErrorCoder interface {
ErrorCode() string
}
```
ErrorCoder may be implemented by an error to cause it to return a particular
RemoteError code when DefaultErrorMapper is used.
#### type ErrorHandler
```go
type ErrorHandler func(Params) error
```
ErrorHandler is like httprouter.Handle except it returns an error which may be
returned as the error body of the response. An ErrorHandler function should not
itself write to the ResponseWriter if it returns an error.
#### type Handler
```go
type Handler struct {
Method string
Path string
Handle httprouter.Handle
}
```
Handler defines a HTTP handler that will handle the given HTTP method at the
given httprouter path
#### type HeaderSetter
```go
type HeaderSetter interface {
SetHeader(http.Header)
}
```
HeaderSetter is the interface checked for by WriteJSON. If implemented on a
value passed to WriteJSON, the SetHeader method will be called to allow it to
set custom headers on the response.
#### type IsZeroer
```go
type IsZeroer interface {
IsZero() bool
}
```
IsZeroer is used when marshaling to determine if a value is zero (see Marshal).
#### type JSONHandler
```go
type JSONHandler func(Params) (interface{}, error)
```
JSONHandler is like httprouter.Handle except that it returns a body (to be
converted to JSON) and an error. The Header parameter can be used to set custom
headers on the response.
#### type Params
```go
type Params struct {
Response http.ResponseWriter
Request *http.Request
PathVar httprouter.Params
// PathPattern holds the path pattern matched by httprouter.
// It is only set where httprequest has the information;
// that is where the call was made by Server.Handler
// or Server.Handlers.
PathPattern string
// Context holds a context for the request. In Go 1.7 and later,
// this should be used in preference to Request.Context.
Context context.Context
}
```
Params holds the parameters provided to an HTTP request.
#### type RemoteError
```go
type RemoteError struct {
// Message holds the error message.
Message string
// Code may hold a code that classifies the error.
Code string `json:",omitempty"`
// Info holds any other information associated with the error.
Info *json.RawMessage `json:",omitempty"`
}
```
RemoteError holds the default type of a remote error used by Client when no
custom error unmarshaler is set. This type is also used by DefaultErrorMapper to
marshal errors in Server.
#### func Errorf
```go
func Errorf(code string, f string, a ...interface{}) *RemoteError
```
Errorf returns a new RemoteError instance that uses the given code and formats
the message with fmt.Sprintf(f, a...). If f is empty and there are no other
arguments, code will also be used for the message.
#### func (*RemoteError) Error
```go
func (e *RemoteError) Error() string
```
Error implements the error interface.
#### func (*RemoteError) ErrorCode
```go
func (e *RemoteError) ErrorCode() string
```
ErrorCode implements ErrorCoder by returning e.Code.
#### type Route
```go
type Route struct{}
```
Route is the type of a field that specifies a routing path and HTTP method. See
Marshal and Unmarshal for details.
#### type Server
```go
type Server struct {
// ErrorMapper holds a function that can convert a Go error
// into a form that can be returned as a JSON body from an HTTP request.
//
// The httpStatus value reports the desired HTTP status.
//
// If the returned errorBody implements HeaderSetter, then
// that method will be called to add custom headers to the request.
//
// If this both this and ErrorWriter are nil, DefaultErrorMapper will be used.
ErrorMapper func(ctxt context.Context, err error) (httpStatus int, errorBody interface{})
// ErrorWriter is a more general form of ErrorMapper. If this
// field is set, ErrorMapper will be ignored and any returned
// errors will be passed to ErrorWriter, which should use
// w to set the HTTP status and write an appropriate
// error response.
ErrorWriter func(ctx context.Context, w http.ResponseWriter, err error)
}
```
Server represents the server side of an HTTP servers, and can be used to create
HTTP handlers although it is not an HTTP handler itself.
#### func (*Server) Handle
```go
func (srv *Server) Handle(f interface{}) Handler
```
Handle converts a function into a Handler. The argument f must be a function of
one of the following six forms, where ArgT must be a struct type acceptable to
Unmarshal and ResultT is a type that can be marshaled as JSON:
func(p Params, arg *ArgT)
func(p Params, arg *ArgT) error
func(p Params, arg *ArgT) (ResultT, error)
func(arg *ArgT)
func(arg *ArgT) error
func(arg *ArgT) (ResultT, error)
When processing a call to the returned handler, the provided parameters are
unmarshaled into a new ArgT value using Unmarshal, then f is called with this
value. If the unmarshaling fails, f will not be called and the unmarshal error
will be written as a JSON response.
As an additional special case to the rules defined in Unmarshal, the tag on an
anonymous field of type Route specifies the method and path to use in the HTTP
request. It should hold two space-separated fields; the first specifies the HTTP
method, the second the URL path to use for the request. If this is given, the
returned handler will hold that method and path, otherwise they will be empty.
If an error is returned from f, it is passed through the error mapper before
writing as a JSON response.
In the third form, when no error is returned, the result is written as a JSON
response with status http.StatusOK. Also in this case, any calls to
Params.Response.Write or Params.Response.WriteHeader will be ignored, as the
response code and data should be defined entirely by the returned result and
error.
Handle will panic if the provided function is not in one of the above forms.
#### func (*Server) HandleErrors
```go
func (srv *Server) HandleErrors(handle ErrorHandler) httprouter.Handle
```
HandleErrors returns a handler that passes any non-nil error returned by handle
through the error mapper and writes it as a JSON response.
Note that the Params argument passed to handle will not have its PathPattern set
as that information is not available.
#### func (*Server) HandleJSON
```go
func (srv *Server) HandleJSON(handle JSONHandler) httprouter.Handle
```
HandleJSON returns a handler that writes the return value of handle as a JSON
response. If handle returns an error, it is passed through the error mapper.
Note that the Params argument passed to handle will not have its PathPattern set
as that information is not available.
#### func (*Server) Handlers
```go
func (srv *Server) Handlers(f interface{}) []Handler
```
Handlers returns a list of handlers that will be handled by the value returned
by the given argument, which must be a function in one of the following forms:
func(p httprequest.Params) (T, context.Context, error)
func(p httprequest.Params, handlerArg I) (T, context.Context, error)
for some type T and some interface type I. Each exported method defined on T
defines a handler, and should be in one of the forms accepted by Server.Handle
with the additional constraint that the argument to each of the handlers must be
compatible with the type I when the second form is used above.
The returned context will be used as the value of Params.Context when Params is
passed to any method. It will also be used when writing an error if the function
returns an error.
Handlers will panic if f is not of the required form, no methods are defined on
T or any method defined on T is not suitable for Handle.
When any of the returned handlers is invoked, f will be called and then the
appropriate method will be called on the value it returns. If specified, the
handlerArg parameter to f will hold the ArgT argument that will be passed to the
handler method.
If T implements io.Closer, its Close method will be called after the request is
completed.
#### func (*Server) WriteError
```go
func (srv *Server) WriteError(ctx context.Context, w http.ResponseWriter, err error)
```
WriteError writes an error to a ResponseWriter and sets the HTTP status code,
using srv.ErrorMapper to determine the actually written response.
It uses WriteJSON to write the error body returned from the ErrorMapper so it is
possible to add custom headers to the HTTP error response by implementing
HeaderSetter.
httprequest-1.2.1/bench_test.go 0000664 0000000 0000000 00000030227 13654526166 0016572 0 ustar 00root root 0000000 0000000 // Copyright 2015 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package httprequest_test
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"testing"
"time"
"github.com/julienschmidt/httprouter"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
)
const dateFormat = "2006-01-02"
type testResult struct {
Key string `json:",omitempty"`
Date string `json:",omitempty"`
Count int64
}
type testParams2Fields struct {
Id string `httprequest:"id,path"`
Limit int `httprequest:"limit,form"`
}
type testParams4Fields struct {
Id string `httprequest:"id,path"`
Limit int `httprequest:"limit,form"`
From dateTime `httprequest:"from,form"`
To dateTime `httprequest:"to,form"`
}
type dateTime struct {
time.Time
}
func (dt *dateTime) UnmarshalText(b []byte) (err error) {
dt.Time, err = time.Parse(dateFormat, string(b))
return
}
type testParams2StringFields struct {
Field0 string `httprequest:",form"`
Field1 string `httprequest:",form"`
}
type testParams4StringFields struct {
Field0 string `httprequest:",form"`
Field1 string `httprequest:",form"`
Field2 string `httprequest:",form"`
Field3 string `httprequest:",form"`
}
type testParams8StringFields struct {
Field0 string `httprequest:",form"`
Field1 string `httprequest:",form"`
Field2 string `httprequest:",form"`
Field3 string `httprequest:",form"`
Field4 string `httprequest:",form"`
Field5 string `httprequest:",form"`
Field6 string `httprequest:",form"`
Field7 string `httprequest:",form"`
}
type testParams16StringFields struct {
Field0 string `httprequest:",form"`
Field1 string `httprequest:",form"`
Field2 string `httprequest:",form"`
Field3 string `httprequest:",form"`
Field4 string `httprequest:",form"`
Field5 string `httprequest:",form"`
Field6 string `httprequest:",form"`
Field7 string `httprequest:",form"`
Field8 string `httprequest:",form"`
Field9 string `httprequest:",form"`
Field10 string `httprequest:",form"`
Field11 string `httprequest:",form"`
Field12 string `httprequest:",form"`
Field13 string `httprequest:",form"`
Field14 string `httprequest:",form"`
Field15 string `httprequest:",form"`
}
func BenchmarkUnmarshal2Fields(b *testing.B) {
params := httprequest.Params{
Request: &http.Request{
Form: url.Values{
"limit": {"2000"},
},
},
PathVar: httprouter.Params{{
Key: "id",
Value: "someid",
}},
}
var arg testParams2Fields
b.ResetTimer()
for i := 0; i < b.N; i++ {
arg = testParams2Fields{}
err := httprequest.Unmarshal(params, &arg)
if err != nil {
b.Fatalf("unmarshal failed: %v", err)
}
}
b.StopTimer()
if !reflect.DeepEqual(arg, testParams2Fields{
Id: "someid",
Limit: 2000,
}) {
b.Errorf("unexpected result: got %#v", arg)
}
}
func BenchmarkHandle2FieldsTrad(b *testing.B) {
results := []testResult{}
benchmarkHandle2Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) {
limit := -1
if limitStr := p.Request.Form.Get("limit"); limitStr != "" {
var err error
limit, err = strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
panic("unreachable")
}
}
if id := p.PathVar.ByName("id"); id == "" {
panic("unreachable")
}
return results, nil
}))
}
func BenchmarkHandle2Fields(b *testing.B) {
results := []testResult{}
benchmarkHandle2Fields(b, testServer.Handle(func(p httprequest.Params, arg *testParams2Fields) ([]testResult, error) {
if arg.Limit <= 0 {
panic("unreachable")
}
return results, nil
}).Handle)
}
func BenchmarkHandle2FieldsUnmarshalOnly(b *testing.B) {
results := []testResult{}
benchmarkHandle2Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) {
var arg testParams2Fields
if err := httprequest.Unmarshal(p, &arg); err != nil {
return nil, err
}
if arg.Limit <= 0 {
panic("unreachable")
}
return results, nil
}))
}
func benchmarkHandle2Fields(b *testing.B, handle func(w http.ResponseWriter, req *http.Request, pvar httprouter.Params)) {
rec := httptest.NewRecorder()
params := httprequest.Params{
Request: &http.Request{
Form: url.Values{
"limit": {"2000"},
},
},
PathVar: httprouter.Params{{
Key: "id",
Value: "someid",
}},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
rec.Body.Reset()
handle(rec, params.Request, params.PathVar)
}
}
func BenchmarkUnmarshal4Fields(b *testing.B) {
fromDate, err1 := time.Parse(dateFormat, "2010-10-10")
toDate, err2 := time.Parse(dateFormat, "2011-11-11")
if err1 != nil || err2 != nil {
b.Fatalf("bad times")
}
type P testParams4Fields
params := httprequest.Params{
Request: &http.Request{
Form: url.Values{
"limit": {"2000"},
"from": {fromDate.Format(dateFormat)},
"to": {toDate.Format(dateFormat)},
},
},
PathVar: httprouter.Params{{
Key: "id",
Value: "someid",
}},
}
var args P
b.ResetTimer()
for i := 0; i < b.N; i++ {
args = P{}
err := httprequest.Unmarshal(params, &args)
if err != nil {
b.Fatalf("unmarshal failed: %v", err)
}
}
b.StopTimer()
if !reflect.DeepEqual(args, P{
Id: "someid",
Limit: 2000,
From: dateTime{fromDate},
To: dateTime{toDate},
}) {
b.Errorf("unexpected result: got %#v", args)
}
}
func BenchmarkHandle4FieldsTrad(b *testing.B) {
results := []testResult{}
benchmarkHandle4Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) {
start, stop, err := parseDateRange(p.Request.Form)
if err != nil {
panic("unreachable")
}
_ = start
_ = stop
limit := -1
if limitStr := p.Request.Form.Get("limit"); limitStr != "" {
limit, err = strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
panic("unreachable")
}
}
if id := p.PathVar.ByName("id"); id == "" {
panic("unreachable")
}
return results, nil
}))
}
// parseDateRange parses a date range as specified in an http
// request. The returned times will be zero if not specified.
func parseDateRange(form url.Values) (start, stop time.Time, err error) {
if v := form.Get("start"); v != "" {
var err error
start, err = time.Parse(dateFormat, v)
if err != nil {
return time.Time{}, time.Time{}, errgo.Newf("invalid 'start' value %q", v)
}
}
if v := form.Get("stop"); v != "" {
var err error
stop, err = time.Parse(dateFormat, v)
if err != nil {
return time.Time{}, time.Time{}, errgo.Newf("invalid 'stop' value %q", v)
}
// Cover all timestamps within the stop day.
stop = stop.Add(24*time.Hour - 1*time.Second)
}
return
}
func BenchmarkHandle4Fields(b *testing.B) {
results := []testResult{}
benchmarkHandle4Fields(b, testServer.Handle(func(p httprequest.Params, arg *testParams4Fields) ([]testResult, error) {
if arg.To.Before(arg.From.Time) {
panic("unreachable")
}
if arg.Limit <= 0 {
panic("unreachable")
}
return results, nil
}).Handle)
}
func BenchmarkHandle4FieldsUnmarshalOnly(b *testing.B) {
results := []testResult{}
benchmarkHandle4Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) {
var arg testParams4Fields
if err := httprequest.Unmarshal(p, &arg); err != nil {
return nil, err
}
if arg.To.Before(arg.From.Time) {
panic("unreachable")
}
if arg.Limit <= 0 {
panic("unreachable")
}
return results, nil
}))
}
func benchmarkHandle4Fields(b *testing.B, handle func(w http.ResponseWriter, req *http.Request, pvar httprouter.Params)) {
// example taken from charmstore changes/published endpoint
fromDate, err1 := time.Parse(dateFormat, "2010-10-10")
toDate, err2 := time.Parse(dateFormat, "2011-11-11")
if err1 != nil || err2 != nil {
b.Fatalf("bad times")
}
rec := httptest.NewRecorder()
params := httprequest.Params{
Request: &http.Request{
Form: url.Values{
"limit": {"2000"},
"from": {fromDate.Format(dateFormat)},
"to": {toDate.Format(dateFormat)},
},
},
PathVar: httprouter.Params{{
Key: "id",
Value: "someid",
}},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
rec.Body.Reset()
handle(rec, params.Request, params.PathVar)
}
}
func BenchmarkHandle2StringFields(b *testing.B) {
benchmarkHandleNFields(b, 2, testServer.Handle(func(p httprequest.Params, arg *testParams2StringFields) error {
return nil
}).Handle)
}
func BenchmarkHandle2StringFieldsUnmarshalOnly(b *testing.B) {
benchmarkHandleNFields(b, 2, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams2StringFields
return httprequest.Unmarshal(p, &arg)
}))
}
func BenchmarkHandle2StringFieldsTrad(b *testing.B) {
benchmarkHandleNFields(b, 2, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams2StringFields
arg.Field0 = p.Request.Form.Get("Field0")
arg.Field1 = p.Request.Form.Get("Field1")
return nil
}))
}
func BenchmarkHandle4StringFields(b *testing.B) {
benchmarkHandleNFields(b, 4, testServer.Handle(func(p httprequest.Params, arg *testParams4StringFields) error {
return nil
}).Handle)
}
func BenchmarkHandle4StringFieldsUnmarshalOnly(b *testing.B) {
benchmarkHandleNFields(b, 4, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams4StringFields
return httprequest.Unmarshal(p, &arg)
}))
}
func BenchmarkHandle4StringFieldsTrad(b *testing.B) {
benchmarkHandleNFields(b, 4, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams4StringFields
arg.Field0 = p.Request.Form.Get("Field0")
arg.Field1 = p.Request.Form.Get("Field1")
arg.Field2 = p.Request.Form.Get("Field2")
arg.Field3 = p.Request.Form.Get("Field3")
return nil
}))
}
func BenchmarkHandle8StringFields(b *testing.B) {
benchmarkHandleNFields(b, 8, testServer.Handle(func(p httprequest.Params, arg *testParams8StringFields) error {
return nil
}).Handle)
}
func BenchmarkHandle8StringFieldsUnmarshalOnly(b *testing.B) {
benchmarkHandleNFields(b, 8, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams8StringFields
return httprequest.Unmarshal(p, &arg)
}))
}
func BenchmarkHandle8StringFieldsTrad(b *testing.B) {
benchmarkHandleNFields(b, 8, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams8StringFields
arg.Field0 = p.Request.Form.Get("Field0")
arg.Field1 = p.Request.Form.Get("Field1")
arg.Field2 = p.Request.Form.Get("Field2")
arg.Field3 = p.Request.Form.Get("Field3")
arg.Field4 = p.Request.Form.Get("Field4")
arg.Field5 = p.Request.Form.Get("Field5")
arg.Field6 = p.Request.Form.Get("Field6")
arg.Field7 = p.Request.Form.Get("Field7")
return nil
}))
}
func BenchmarkHandle16StringFields(b *testing.B) {
benchmarkHandleNFields(b, 16, testServer.Handle(func(p httprequest.Params, arg *testParams16StringFields) error {
return nil
}).Handle)
}
func BenchmarkHandle16StringFieldsUnmarshalOnly(b *testing.B) {
benchmarkHandleNFields(b, 16, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams16StringFields
return httprequest.Unmarshal(p, &arg)
}))
}
func BenchmarkHandle16StringFieldsTrad(b *testing.B) {
benchmarkHandleNFields(b, 16, testServer.HandleErrors(func(p httprequest.Params) error {
var arg testParams16StringFields
arg.Field0 = p.Request.Form.Get("Field0")
arg.Field1 = p.Request.Form.Get("Field1")
arg.Field2 = p.Request.Form.Get("Field2")
arg.Field3 = p.Request.Form.Get("Field3")
arg.Field4 = p.Request.Form.Get("Field4")
arg.Field5 = p.Request.Form.Get("Field5")
arg.Field6 = p.Request.Form.Get("Field6")
arg.Field7 = p.Request.Form.Get("Field7")
arg.Field8 = p.Request.Form.Get("Field8")
arg.Field9 = p.Request.Form.Get("Field9")
arg.Field10 = p.Request.Form.Get("Field10")
arg.Field11 = p.Request.Form.Get("Field11")
arg.Field12 = p.Request.Form.Get("Field12")
arg.Field13 = p.Request.Form.Get("Field13")
arg.Field14 = p.Request.Form.Get("Field14")
arg.Field15 = p.Request.Form.Get("Field15")
return nil
}))
}
func benchmarkHandleNFields(b *testing.B, n int, handle func(w http.ResponseWriter, req *http.Request, pvar httprouter.Params)) {
form := make(url.Values)
for i := 0; i < n; i++ {
form[fmt.Sprint("Field", i)] = []string{fmt.Sprintf("field %d", i)}
}
rec := httptest.NewRecorder()
params := httprequest.Params{
Request: &http.Request{
Form: form,
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
rec.Body.Reset()
handle(rec, params.Request, params.PathVar)
}
}
httprequest-1.2.1/client.go 0000664 0000000 0000000 00000024054 13654526166 0015733 0 ustar 00root root 0000000 0000000 // Copyright 2015 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package httprequest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
"gopkg.in/errgo.v1"
)
// Doer is implemented by HTTP client packages
// to make an HTTP request. It is notably implemented
// by http.Client and httpbakery.Client.
type Doer interface {
Do(req *http.Request) (*http.Response, error)
}
// DoerWithContext is implemented by HTTP clients that can use a context
// with the HTTP request.
type DoerWithContext interface {
DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error)
}
// Client represents a client that can invoke httprequest endpoints.
type Client struct {
// BaseURL holds the base URL to use when making
// HTTP requests.
BaseURL string
// Doer holds a value that will be used to actually
// make the HTTP request. If it is nil, http.DefaultClient
// will be used instead. If Doer implements DoerWithContext,
// DoWithContext will be used instead.
Doer Doer
// If a request returns an HTTP response that signifies an
// error, UnmarshalError is used to unmarshal the response into
// an appropriate error. See ErrorUnmarshaler for a convenient
// way to create an UnmarshalError function for a given type. If
// this is nil, DefaultErrorUnmarshaler will be used.
UnmarshalError func(resp *http.Response) error
}
// Call invokes the endpoint implied by the given params,
// which should be of the form accepted by the ArgT
// argument to a function passed to Handle, and
// unmarshals the response into the given response parameter,
// which should be a pointer to the response value.
//
// If params implements the HeaderSetter interface, its SetHeader method
// will be called to add additional headers to the HTTP request.
//
// If resp is nil, the response will be ignored if the
// request was successful.
//
// If resp is of type **http.Response, instead of unmarshaling
// into it, its element will be set to the returned HTTP
// response directly and the caller is responsible for
// closing its Body field.
//
// Any error that c.UnmarshalError or c.Doer returns will not
// have its cause masked.
//
// If the request returns a response with a status code signifying
// success, but the response could not be unmarshaled, a
// *DecodeResponseError will be returned holding the response. Note that if
// the request returns an error status code, the Client.UnmarshalError
// function is responsible for doing this if desired (the default error
// unmarshal functions do).
func (c *Client) Call(ctx context.Context, params, resp interface{}) error {
return c.CallURL(ctx, c.BaseURL, params, resp)
}
// CallURL is like Call except that the given URL is used instead of
// c.BaseURL.
func (c *Client) CallURL(ctx context.Context, url string, params, resp interface{}) error {
rt, err := getRequestType(reflect.TypeOf(params))
if err != nil {
return errgo.Mask(err)
}
if rt.method == "" {
return errgo.Newf("type %T has no httprequest.Route field", params)
}
reqURL, err := appendURL(url, rt.path)
if err != nil {
return errgo.Mask(err)
}
req, err := Marshal(reqURL.String(), rt.method, params)
if err != nil {
return errgo.Mask(err)
}
return c.Do(ctx, req, resp)
}
// Do sends the given request and unmarshals its JSON
// result into resp, which should be a pointer to the response value.
// If an error status is returned, the error will be unmarshaled
// as in Client.Call.
//
// If resp is nil, the response will be ignored if the response was
// successful.
//
// If resp is of type **http.Response, instead of unmarshaling
// into it, its element will be set to the returned HTTP
// response directly and the caller is responsible for
// closing its Body field.
//
// Any error that c.UnmarshalError or c.Doer returns will not
// have its cause masked.
//
// If req.URL does not have a host part it will be treated as relative to
// c.BaseURL. req.URL will be updated to the actual URL used.
//
// If the response cannot by unmarshaled, a *DecodeResponseError
// will be returned holding the response from the request.
// the entire response body.
func (c *Client) Do(ctx context.Context, req *http.Request, resp interface{}) error {
if req.URL.Host == "" {
var err error
req.URL, err = appendURL(c.BaseURL, req.URL.String())
if err != nil {
return errgo.Mask(err)
}
}
doer := c.Doer
if doer == nil {
doer = http.DefaultClient
}
var httpResp *http.Response
var err error
if ctxDoer, ok := doer.(DoerWithContext); ok {
httpResp, err = ctxDoer.DoWithContext(ctx, req)
} else {
httpResp, err = doer.Do(req.WithContext(ctx))
}
if err != nil {
return errgo.Mask(urlError(err, req), errgo.Any)
}
return c.unmarshalResponse(httpResp, resp)
}
// Get is a convenience method that uses c.Do to issue a GET request to
// the given URL. If the given URL does not have a host part then it will
// be treated as relative to c.BaseURL.
func (c *Client) Get(ctx context.Context, url string, resp interface{}) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return errgo.Notef(err, "cannot make request")
}
return c.Do(ctx, req, resp)
}
// unmarshalResponse unmarshals an HTTP response into the given value.
func (c *Client) unmarshalResponse(httpResp *http.Response, resp interface{}) error {
if 200 <= httpResp.StatusCode && httpResp.StatusCode < 300 {
if respPt, ok := resp.(**http.Response); ok {
*respPt = httpResp
return nil
}
defer httpResp.Body.Close()
if err := UnmarshalJSONResponse(httpResp, resp); err != nil {
return errgo.Mask(urlError(err, httpResp.Request), isDecodeResponseError)
}
return nil
}
defer httpResp.Body.Close()
errUnmarshaler := c.UnmarshalError
if errUnmarshaler == nil {
errUnmarshaler = DefaultErrorUnmarshaler
}
err := errUnmarshaler(httpResp)
if err == nil {
err = errgo.Newf("unexpected HTTP response status: %s", httpResp.Status)
}
return errgo.Mask(urlError(err, httpResp.Request), errgo.Any)
}
// ErrorUnmarshaler returns a function which will unmarshal error
// responses into new values of the same type as template. The argument
// must be a pointer. A new instance of it is created every time the
// returned function is called.
//
// If the error cannot by unmarshaled, the function will return an
// *HTTPResponseError holding the response from the request.
func ErrorUnmarshaler(template error) func(*http.Response) error {
t := reflect.TypeOf(template)
if t.Kind() != reflect.Ptr {
panic(errgo.Newf("cannot unmarshal errors into value of type %T", template))
}
t = t.Elem()
return func(resp *http.Response) error {
if 300 <= resp.StatusCode && resp.StatusCode < 400 {
// It's a redirection error.
loc, _ := resp.Location()
return newDecodeResponseError(resp, nil, fmt.Errorf("unexpected redirect (status %s) from %q to %q", resp.Status, resp.Request.URL, loc))
}
errv := reflect.New(t)
if err := UnmarshalJSONResponse(resp, errv.Interface()); err != nil {
return errgo.NoteMask(err, fmt.Sprintf("cannot unmarshal error response (status %s)", resp.Status), isDecodeResponseError)
}
return errv.Interface().(error)
}
}
// UnmarshalJSONResponse unmarshals the given HTTP response
// into x, which should be a pointer to the result to be
// unmarshaled into.
//
// If the response cannot be unmarshaled, an error of type
// *DecodeResponseError will be returned.
func UnmarshalJSONResponse(resp *http.Response, x interface{}) error {
if x == nil {
return nil
}
if !isJSONMediaType(resp.Header) {
fancyErr := newFancyDecodeError(resp.Header, resp.Body)
return newDecodeResponseError(resp, fancyErr.body, fancyErr)
}
// Read enough data that we can produce a plausible-looking
// possibly-truncated response body in the error.
var buf bytes.Buffer
n, err := io.Copy(&buf, io.LimitReader(resp.Body, int64(maxErrorBodySize)))
bodyData := buf.Bytes()
if err != nil {
return newDecodeResponseError(resp, bodyData, errgo.Notef(err, "error reading response body"))
}
if n < int64(maxErrorBodySize) {
// We've read all the data; unmarshal it.
if err := json.Unmarshal(bodyData, x); err != nil {
return newDecodeResponseError(resp, bodyData, err)
}
return nil
}
// The response is longer than maxErrorBodySize; stitch the read
// bytes together with the body so that we can still read
// bodies larger than maxErrorBodySize.
dec := json.NewDecoder(io.MultiReader(&buf, resp.Body))
// Try to read all the body so that we can reuse the
// connection, but don't try *too* hard. Note that the
// usual number of additional bytes is 1 (a single newline
// after the JSON).
defer io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8*1024))
if err := dec.Decode(x); err != nil {
return newDecodeResponseError(resp, bodyData, err)
}
return nil
}
// appendURL returns the result of combining the
// given base URL and relative URL.
//
// The path of the relative URL will be appended
// to the base URL, separated by a slash (/) if
// needed.
//
// Any query parameters will be concatenated together.
//
// appendURL will return an error if relURLStr contains
// a host name.
func appendURL(baseURLStr, relURLStr string) (*url.URL, error) {
b, err := url.Parse(baseURLStr)
if err != nil {
return nil, errgo.Notef(err, "cannot parse %q", baseURLStr)
}
r, err := url.Parse(relURLStr)
if err != nil {
return nil, errgo.Notef(err, "cannot parse %q", relURLStr)
}
if r.Host != "" {
return nil, errgo.Newf("relative URL specifies a host")
}
if r.Path != "" {
b.Path = strings.TrimSuffix(b.Path, "/") + "/" + strings.TrimPrefix(r.Path, "/")
}
if r.RawQuery != "" {
if b.RawQuery != "" {
b.RawQuery += "&" + r.RawQuery
} else {
b.RawQuery = r.RawQuery
}
}
return b, nil
}
func urlError(err error, req *http.Request) error {
_, ok := errgo.Cause(err).(*url.Error)
if ok {
// The error is already sufficiently annotated.
return err
}
// Convert the method to mostly lower case to match net/http's behaviour
// so we don't get silly divergence of messages.
method := req.Method[:1] + strings.ToLower(req.Method[1:])
return errgo.NoteMask(err, fmt.Sprintf("%s %s", method, req.URL), errgo.Any)
}
httprequest-1.2.1/client_test.go 0000664 0000000 0000000 00000054014 13654526166 0016771 0 ustar 00root root 0000000 0000000 package httprequest_test
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/julienschmidt/httprouter"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
)
var callTests = []struct {
about string
client httprequest.Client
req interface{}
requestUUID string
expectError string
assertError func(c *qt.C, err error)
expectResp interface{}
}{{
about: "GET success",
req: &chM1Req{
P: "hello",
},
expectResp: &chM1Resp{"hello"},
}, {
about: "GET with nil response",
req: &chM1Req{
P: "hello",
},
}, {
about: "POST success",
req: &chM2Req{
P: "hello",
Body: struct{ I int }{999},
},
expectResp: &chM2Resp{"hello", 999},
}, {
about: "GET marshal error",
req: 123,
expectError: `type is not pointer to struct`,
}, {
about: "error response",
req: &chInvalidM2Req{
P: "hello",
Body: struct{ I bool }{true},
},
expectError: `Post http:.*: cannot unmarshal parameters: cannot unmarshal into field Body: cannot unmarshal request body: json: cannot unmarshal .*`,
assertError: func(c *qt.C, err error) {
c.Assert(errgo.Cause(err), qt.Satisfies, isRemoteError)
err1 := errgo.Cause(err).(*httprequest.RemoteError)
c.Assert(err1.Code, qt.Equals, "bad request")
c.Assert(err1.Message, qt.Matches, `cannot unmarshal parameters: cannot unmarshal into field Body: cannot unmarshal request body: json: cannot unmarshal .*`)
},
}, {
about: "error unmarshaler returns nil",
client: httprequest.Client{
UnmarshalError: func(*http.Response) error {
return nil
},
},
req: &chM3Req{},
expectError: `Get http://.*/m3: unexpected HTTP response status: 500 Internal Server Error`,
}, {
about: "unexpected redirect",
req: &chM2RedirectM2Req{
Body: struct{ I int }{999},
},
expectResp: &chM2Resp{"foo", 999},
}, {
about: "bad content in successful response",
req: &chM4Req{},
expectResp: new(int),
expectError: `Get http://.*/m4: unexpected content type text/plain; want application/json; content: bad response`,
assertError: func(c *qt.C, err error) {
err1, ok := errgo.Cause(err).(*httprequest.DecodeResponseError)
c.Assert(ok, qt.Equals, true, qt.Commentf("error not of type *httprequest.DecodeResponseError (%T)", errgo.Cause(err)))
c.Assert(err1.Response, qt.Not(qt.IsNil))
data, err := ioutil.ReadAll(err1.Response.Body)
c.Assert(err, qt.Equals, nil)
c.Assert(string(data), qt.Equals, "bad response")
},
}, {
about: "bad content in error response",
req: &chM5Req{},
expectResp: new(int),
expectError: `Get http://.*/m5: cannot unmarshal error response \(status 418 I'm a teapot\): unexpected content type text/plain; want application/json; content: bad error value`,
assertError: func(c *qt.C, err error) {
err1, ok := errgo.Cause(err).(*httprequest.DecodeResponseError)
c.Assert(ok, qt.Equals, true, qt.Commentf("error not of type *httprequest.DecodeResponseError (%T)", errgo.Cause(err)))
c.Assert(err1.Response, qt.Not(qt.IsNil))
data, err := ioutil.ReadAll(err1.Response.Body)
c.Assert(err, qt.Equals, nil)
c.Assert(string(data), qt.Equals, "bad error value")
c.Assert(err1.Response.StatusCode, qt.Equals, http.StatusTeapot)
},
}, {
about: "doer with context",
client: httprequest.Client{
Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) {
if ctx == nil {
panic("Do called when DoWithContext expected")
}
return http.DefaultClient.Do(req.WithContext(ctx))
}),
},
req: &chM2Req{
P: "hello",
Body: struct{ I int }{999},
},
expectResp: &chM2Resp{"hello", 999},
}, {
about: "doer with context and body",
client: httprequest.Client{
Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) {
if ctx == nil {
panic("Do called when DoWithContext expected")
}
return http.DefaultClient.Do(req.WithContext(ctx))
}),
},
req: &chM2Req{
P: "hello",
Body: struct{ I int }{999},
},
expectResp: &chM2Resp{"hello", 999},
}, {
about: "doer with context and body but no body",
client: httprequest.Client{
Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) {
if ctx == nil {
panic("Do called when DoWithContext expected")
}
return http.DefaultClient.Do(req.WithContext(ctx))
}),
},
req: &chM1Req{
P: "hello",
},
expectResp: &chM1Resp{"hello"},
}}
func TestCall(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
for _, test := range callTests {
c.Run(test.about, func(c *qt.C) {
var resp interface{}
if test.expectResp != nil {
resp = reflect.New(reflect.TypeOf(test.expectResp).Elem()).Interface()
}
client := test.client
client.BaseURL = srv.URL
ctx := context.Background()
err := client.Call(ctx, test.req, resp)
if test.expectError != "" {
c.Logf("err %v", errgo.Details(err))
c.Check(err, qt.ErrorMatches, test.expectError)
if test.assertError != nil {
test.assertError(c, err)
}
return
}
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, test.expectResp)
})
}
}
func TestCallURLNoRequestPath(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
var client httprequest.Client
req := struct {
httprequest.Route `httprequest:"GET"`
chM1Req
}{
chM1Req: chM1Req{
P: "hello",
},
}
var resp chM1Resp
err := client.CallURL(context.Background(), srv.URL+"/m1/:P", &req, &resp)
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, chM1Resp{"hello"})
}
func mustNewRequest(url string, method string, body io.Reader) *http.Request {
return mustNewRequestWithHeader(url, method, body, http.Header{
"Content-Type": []string{"application/json"},
})
}
func mustNewRequestWithHeader(url string, method string, body io.Reader, hdr http.Header) *http.Request {
req, err := http.NewRequest(method, url, body)
if err != nil {
panic(err)
}
for k, v := range hdr {
req.Header[k] = append(req.Header[k], v...)
}
return req
}
var doTests = []struct {
about string
client httprequest.Client
request *http.Request
requestUUID string
expectError string
expectCause interface{}
expectResp interface{}
}{{
about: "GET success",
request: mustNewRequest("/m1/hello", "GET", nil),
expectResp: &chM1Resp{"hello"},
}, {
about: "appendURL error",
request: mustNewRequest("/m1/hello", "GET", nil),
client: httprequest.Client{
BaseURL: ":::",
},
expectError: `cannot parse ":::": parse "?:::"?: missing protocol scheme`,
}, {
about: "Do returns error",
client: httprequest.Client{
Doer: doerFunc(func(req *http.Request) (*http.Response, error) {
return nil, errgo.Newf("an error")
}),
},
request: mustNewRequest("/m2/foo", "POST", strings.NewReader(`{"I": 999}`)),
expectError: "Post http://.*/m2/foo: an error",
}, {
about: "doer with context",
client: httprequest.Client{
Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) {
if ctx == nil {
panic("Do called when DoWithContext expected")
}
return http.DefaultClient.Do(req.WithContext(ctx))
}),
},
request: mustNewRequest("/m2/foo", "POST", strings.NewReader(`{"I": 999}`)),
expectResp: &chM2Resp{"foo", 999},
}}
func newInt64(i int64) *int64 {
return &i
}
func TestDo(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
for _, test := range doTests {
test := test
c.Run(test.about, func(c *qt.C) {
var resp interface{}
if test.expectResp != nil {
resp = reflect.New(reflect.TypeOf(test.expectResp).Elem()).Interface()
}
client := test.client
if client.BaseURL == "" {
client.BaseURL = srv.URL
}
ctx := context.Background()
err := client.Do(ctx, test.request, resp)
if test.expectError != "" {
c.Assert(err, qt.ErrorMatches, test.expectError)
if test.expectCause != nil {
c.Assert(errgo.Cause(err), qt.DeepEquals, test.expectCause)
}
return
}
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, test.expectResp)
})
}
}
func TestDoWithHTTPReponse(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
client := &httprequest.Client{
BaseURL: srv.URL,
}
var resp *http.Response
err := client.Get(context.Background(), "/m1/foo", &resp)
c.Assert(err, qt.Equals, nil)
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
c.Assert(err, qt.Equals, nil)
c.Assert(string(data), qt.Equals, `{"P":"foo"}`)
}
func TestDoWithHTTPReponseAndError(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
var doer closeCountingDoer // Also check the body is closed.
client := &httprequest.Client{
BaseURL: srv.URL,
Doer: &doer,
}
var resp *http.Response
err := client.Get(context.Background(), "/m3", &resp)
c.Assert(resp, qt.IsNil)
c.Assert(err, qt.ErrorMatches, `Get http:.*/m3: m3 error`)
c.Assert(doer.openedBodies, qt.Equals, 1)
c.Assert(doer.closedBodies, qt.Equals, 1)
}
func TestCallWithHTTPResponse(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
client := &httprequest.Client{
BaseURL: srv.URL,
}
var resp *http.Response
err := client.Call(context.Background(), &chM1Req{
P: "foo",
}, &resp)
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
c.Assert(err, qt.Equals, nil)
c.Assert(string(data), qt.Equals, `{"P":"foo"}`)
}
func TestCallClosesResponseBodyOnSuccess(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
var doer closeCountingDoer
client := &httprequest.Client{
BaseURL: srv.URL,
Doer: &doer,
}
var resp chM1Resp
err := client.Call(context.Background(), &chM1Req{
P: "foo",
}, &resp)
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"})
c.Assert(doer.openedBodies, qt.Equals, 1)
c.Assert(doer.closedBodies, qt.Equals, 1)
}
func TestCallClosesResponseBodyOnError(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
var doer closeCountingDoer
client := &httprequest.Client{
BaseURL: srv.URL,
Doer: &doer,
}
err := client.Call(context.Background(), &chM3Req{}, nil)
c.Assert(err, qt.ErrorMatches, ".*m3 error")
c.Assert(doer.openedBodies, qt.Equals, 1)
c.Assert(doer.closedBodies, qt.Equals, 1)
}
func TestDoClosesResponseBodyOnSuccess(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
var doer closeCountingDoer
client := &httprequest.Client{
BaseURL: srv.URL,
Doer: &doer,
}
req, err := http.NewRequest("GET", "/m1/foo", nil)
c.Assert(err, qt.Equals, nil)
var resp chM1Resp
err = client.Do(context.Background(), req, &resp)
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"})
c.Assert(doer.openedBodies, qt.Equals, 1)
c.Assert(doer.closedBodies, qt.Equals, 1)
}
func TestDoClosesResponseBodyOnError(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
var doer closeCountingDoer
client := &httprequest.Client{
BaseURL: srv.URL,
Doer: &doer,
}
req, err := http.NewRequest("GET", "/m3", nil)
c.Assert(err, qt.Equals, nil)
err = client.Do(context.Background(), req, nil)
c.Assert(err, qt.ErrorMatches, ".*m3 error")
c.Assert(doer.openedBodies, qt.Equals, 1)
c.Assert(doer.closedBodies, qt.Equals, 1)
}
func TestGet(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
client := httprequest.Client{
BaseURL: srv.URL,
}
var resp chM1Resp
err := client.Get(context.Background(), "/m1/foo", &resp)
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"})
}
func TestGetNoBaseURL(t *testing.T) {
c := qt.New(t)
defer c.Done()
srv := newServer()
c.Defer(srv.Close)
client := httprequest.Client{}
var resp chM1Resp
err := client.Get(context.Background(), srv.URL+"/m1/foo", &resp)
c.Assert(err, qt.Equals, nil)
c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"})
}
func TestUnmarshalJSONResponseWithBodyReadError(t *testing.T) {
c := qt.New(t)
resp := &http.Response{
Header: http.Header{
"Content-Type": {"application/json"},
},
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(io.MultiReader(
strings.NewReader(`{"one": "two"}`),
errorReader("some bad read"),
)),
}
var val map[string]string
err := httprequest.UnmarshalJSONResponse(resp, &val)
c.Assert(err, qt.ErrorMatches, `error reading response body: some bad read`)
c.Assert(val, qt.IsNil)
assertDecodeResponseError(c, err, http.StatusOK, `{"one": "two"}`)
}
var unmarshalJSONResponseWithVariedJSONContentTypesTests = []struct {
contentType string
expectError bool
}{{
contentType: "application/json",
}, {
contentType: "application/json+other",
}, {
contentType: "application/vnd.schemaregistry.v1+json",
}, {
contentType: "other/json",
expectError: true,
}, {
contentType: "other/jsonx",
expectError: true,
}, {
contentType: "other/xjson",
expectError: true,
}, {
contentType: "other/other+xjson",
expectError: true,
}, {
contentType: "other/other+jsonx",
expectError: true,
}, {
contentType: "application/other+json+foo",
}, {
contentType: "application/other+json",
}, {
contentType: "application/other+json+",
}, {
contentType: "application/+json+",
}}
func TestUnmarshalJSONResponseWithVariedJSONContentTypes(t *testing.T) {
c := qt.New(t)
for _, test := range unmarshalJSONResponseWithVariedJSONContentTypesTests {
c.Run(test.contentType, func(c *qt.C) {
resp := &http.Response{
Header: http.Header{
"Content-Type": {test.contentType},
},
StatusCode: http.StatusTeapot,
Body: ioutil.NopCloser(strings.NewReader(`{}`)),
}
var val map[string]string
err := httprequest.UnmarshalJSONResponse(resp, &val)
if !test.expectError {
c.Assert(err, qt.IsNil)
return
}
c.Assert(err, qt.ErrorMatches, `unexpected content type `+regexp.QuoteMeta(test.contentType)+`; want application/json; content: "{}"`)
c.Assert(val, qt.IsNil)
assertDecodeResponseError(c, err, http.StatusTeapot, `{}`)
})
}
}
func TestUnmarshalJSONResponseWithErrorAndLargeBody(t *testing.T) {
c := qt.New(t)
defer c.Done()
c.Patch(httprequest.MaxErrorBodySize, 11)
resp := &http.Response{
Header: http.Header{
"Content-Type": {"foo/bar"},
},
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`123456789 123456789`)),
}
var val map[string]string
err := httprequest.UnmarshalJSONResponse(resp, &val)
c.Assert(err, qt.ErrorMatches, `unexpected content type foo/bar; want application/json; content: "123456789 1"`)
c.Assert(val, qt.IsNil)
assertDecodeResponseError(c, err, http.StatusOK, `123456789 1`)
}
func TestUnmarshalJSONResponseWithLargeBody(t *testing.T) {
c := qt.New(t)
defer c.Done()
c.Patch(httprequest.MaxErrorBodySize, 11)
resp := &http.Response{
Header: http.Header{
"Content-Type": {"application/json"},
},
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`"23456789 123456789"`)),
}
var val string
err := httprequest.UnmarshalJSONResponse(resp, &val)
c.Assert(err, qt.Equals, nil)
c.Assert(val, qt.Equals, "23456789 123456789")
}
func TestUnmarshalJSONWithDecodeError(t *testing.T) {
c := qt.New(t)
resp := &http.Response{
Header: http.Header{
"Content-Type": {"application/json"},
},
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`{"one": "two"}`)),
}
var val chan string
err := httprequest.UnmarshalJSONResponse(resp, &val)
c.Assert(err, qt.ErrorMatches, `json: cannot unmarshal object into Go value of type chan string`)
c.Assert(val, qt.IsNil)
assertDecodeResponseError(c, err, http.StatusOK, `{"one": "two"}`)
}
func TestUnmarshalJSONWithDecodeErrorAndLargeBody(t *testing.T) {
c := qt.New(t)
defer c.Done()
c.Patch(httprequest.MaxErrorBodySize, 11)
resp := &http.Response{
Header: http.Header{
"Content-Type": {"application/json"},
},
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`"23456789 123456789"`)),
}
var val chan string
err := httprequest.UnmarshalJSONResponse(resp, &val)
c.Assert(err, qt.ErrorMatches, `json: cannot unmarshal string into Go value of type chan string`)
c.Assert(val, qt.IsNil)
assertDecodeResponseError(c, err, http.StatusOK, `"23456789 1`)
}
func assertDecodeResponseError(c *qt.C, err error, status int, body string) {
err1, ok := errgo.Cause(err).(*httprequest.DecodeResponseError)
c.Assert(ok, qt.Equals, true, qt.Commentf("error not of type *httprequest.DecodeResponseError (%T)", errgo.Cause(err)))
data, err := ioutil.ReadAll(err1.Response.Body)
c.Assert(err, qt.Equals, nil)
c.Assert(err1.Response.StatusCode, qt.Equals, status)
c.Assert(string(data), qt.Equals, body)
}
func newServer() *httptest.Server {
f := func(p httprequest.Params) (clientHandlers, context.Context, error) {
return clientHandlers{}, p.Context, nil
}
handlers := testServer.Handlers(f)
router := httprouter.New()
for _, h := range handlers {
router.Handle(h.Method, h.Path, h.Handle)
}
return httptest.NewServer(router)
}
var appendURLTests = []struct {
u string
p string
expect string
expectError string
}{{
u: "http://foo",
p: "bar",
expect: "http://foo/bar",
}, {
u: "http://foo",
p: "/bar",
expect: "http://foo/bar",
}, {
u: "http://foo/",
p: "bar",
expect: "http://foo/bar",
}, {
u: "http://foo/",
p: "/bar",
expect: "http://foo/bar",
}, {
u: "",
p: "bar",
expect: "/bar",
}, {
u: "http://xxx",
p: "",
expect: "http://xxx",
}, {
u: "http://xxx.com",
p: "http://foo.com",
expectError: "relative URL specifies a host",
}, {
u: "http://xxx.com/a/b",
p: "foo?a=45&b=c",
expect: "http://xxx.com/a/b/foo?a=45&b=c",
}, {
u: "http://xxx.com",
p: "?a=45&b=c",
expect: "http://xxx.com?a=45&b=c",
}, {
u: "http://xxx.com/a?z=w",
p: "foo?a=45&b=c",
expect: "http://xxx.com/a/foo?z=w&a=45&b=c",
}, {
u: "http://xxx.com?z=w",
p: "/a/b/c",
expect: "http://xxx.com/a/b/c?z=w",
}}
func TestAppendURL(t *testing.T) {
c := qt.New(t)
for _, test := range appendURLTests {
test := test
c.Run(fmt.Sprintf("%s_%s", test.u, test.p), func(c *qt.C) {
u, err := httprequest.AppendURL(test.u, test.p)
if test.expectError != "" {
c.Assert(u, qt.IsNil)
c.Assert(err, qt.ErrorMatches, test.expectError)
} else {
c.Assert(err, qt.Equals, nil)
c.Assert(u.String(), qt.Equals, test.expect)
}
})
}
}
type clientHandlers struct{}
type chM1Req struct {
httprequest.Route `httprequest:"GET /m1/:P"`
P string `httprequest:",path"`
}
type chM1Resp struct {
P string
}
func (clientHandlers) M1(p *chM1Req) (*chM1Resp, error) {
return &chM1Resp{p.P}, nil
}
type chM2Req struct {
httprequest.Route `httprequest:"POST /m2/:P"`
P string `httprequest:",path"`
Body struct {
I int
} `httprequest:",body"`
}
type chInvalidM2Req struct {
httprequest.Route `httprequest:"POST /m2/:P"`
P string `httprequest:",path"`
Body struct {
I bool
} `httprequest:",body"`
}
type chM2RedirectM2Req struct {
httprequest.Route `httprequest:"POST /m2/foo//"`
Body struct {
I int
} `httprequest:",body"`
}
type chM2Resp struct {
P string
Arg int
}
func (clientHandlers) M2(p *chM2Req) (*chM2Resp, error) {
return &chM2Resp{p.P, p.Body.I}, nil
}
type chM3Req struct {
httprequest.Route `httprequest:"GET /m3"`
}
func (clientHandlers) M3(p *chM3Req) error {
return errgo.New("m3 error")
}
type chM4Req struct {
httprequest.Route `httprequest:"GET /m4"`
}
func (clientHandlers) M4(p httprequest.Params, _ *chM4Req) {
p.Response.Write([]byte("bad response"))
}
type chM5Req struct {
httprequest.Route `httprequest:"GET /m5"`
}
func (clientHandlers) M5(p httprequest.Params, _ *chM5Req) {
p.Response.WriteHeader(http.StatusTeapot)
p.Response.Write([]byte("bad error value"))
}
type chContentLengthReq struct {
httprequest.Route `httprequest:"PUT /content-length"`
}
func (clientHandlers) ContentLength(rp httprequest.Params, p *chContentLengthReq) (int64, error) {
return rp.Request.ContentLength, nil
}
type doerFunc func(req *http.Request) (*http.Response, error)
func (f doerFunc) Do(req *http.Request) (*http.Response, error) {
return f(req)
}
type doerWithContextFunc func(ctx context.Context, req *http.Request) (*http.Response, error)
func (f doerWithContextFunc) Do(req *http.Request) (*http.Response, error) {
return f(nil, req)
}
func (f doerWithContextFunc) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) {
if ctx == nil {
panic("unexpected nil context")
}
return f(ctx, req)
}
type closeCountingDoer struct {
// openBodies records the number of response bodies
// that have been returned.
openedBodies int
// closedBodies records the number of response bodies
// that have been closed.
closedBodies int
}
func (doer *closeCountingDoer) Do(req *http.Request) (*http.Response, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
resp.Body = &closeCountingReader{
doer: doer,
ReadCloser: resp.Body,
}
doer.openedBodies++
return resp, nil
}
type closeCountingReader struct {
doer *closeCountingDoer
io.ReadCloser
}
func (r *closeCountingReader) Close() error {
r.doer.closedBodies++
return r.ReadCloser.Close()
}
// largeReader implements a reader that produces up to total bytes
// in 1 byte reads.
type largeReader struct {
byte byte
total int
n int
}
func (r *largeReader) Read(buf []byte) (int, error) {
if r.n >= r.total {
return 0, io.EOF
}
r.n++
return copy(buf, []byte{r.byte}), nil
}
func (r *largeReader) Seek(offset int64, whence int) (int64, error) {
if offset != 0 || whence != 0 {
panic("unexpected seek")
}
r.n = 0
return 0, nil
}
func (r *largeReader) Close() error {
// By setting n to zero, we ensure that if there's
// a concurrent read, it will also read from n
// and so the race detector should pick up the
// problem.
r.n = 0
return nil
}
func isRemoteError(err error) bool {
_, ok := err.(*httprequest.RemoteError)
return ok
}
httprequest-1.2.1/cmd/ 0000775 0000000 0000000 00000000000 13654526166 0014664 5 ustar 00root root 0000000 0000000 httprequest-1.2.1/cmd/httprequest-generate-client/ 0000775 0000000 0000000 00000000000 13654526166 0022320 5 ustar 00root root 0000000 0000000 httprequest-1.2.1/cmd/httprequest-generate-client/main.go 0000664 0000000 0000000 00000017034 13654526166 0023600 0 ustar 00root root 0000000 0000000 // +build go1.8
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/format"
"go/parser"
"go/token"
"go/types"
"io/ioutil"
"os"
"strings"
"text/template"
"golang.org/x/tools/go/packages"
"gopkg.in/errgo.v1"
)
// TODO:
// - generate exported types if the parameter/response types aren't exported?
// - deal with literal interface and struct types.
// - copy doc comments from server methods.
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: httprequest-generate server-package server-type client-type\n")
os.Exit(2)
}
flag.Parse()
if flag.NArg() != 3 {
flag.Usage()
}
serverPkg, serverType, clientType := flag.Arg(0), flag.Arg(1), flag.Arg(2)
if err := generate(serverPkg, serverType, clientType); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
type templateArg struct {
PkgName string
Imports []string
Methods []method
ClientType string
}
var code = template.Must(template.New("").Parse(`
// The code in this file was automatically generated by running httprequest-generate-client.
// DO NOT EDIT
package {{.PkgName}}
import (
{{range .Imports}}{{printf "%q" .}}
{{end}}
)
type {{.ClientType}} struct {
Client httprequest.Client
}
{{range .Methods}}
{{if .RespType}}
{{.Doc}}
func (c *{{$.ClientType}}) {{.Name}}(ctx context.Context, p *{{.ParamType}}) ({{.RespType}}, error) {
var r {{.RespType}}
err := c.Client.Call(ctx, p, &r)
return r, err
}
{{else}}
{{.Doc}}
func (c *{{$.ClientType}}) {{.Name}}(ctx context.Context, p *{{.ParamType}}) (error) {
return c.Client.Call(ctx, p, nil)
}
{{end}}
{{end}}
`))
func generate(serverPkgPath, serverType, clientType string) error {
currentDir, err := os.Getwd()
if err != nil {
return err
}
localPkg, err := build.Import(".", currentDir, 0)
if err != nil {
return errgo.Notef(err, "cannot open package in current directory")
}
serverPkg, err := build.Import(serverPkgPath, currentDir, 0)
if err != nil {
return errgo.Notef(err, "cannot open %q", serverPkgPath)
}
methods, imports, err := serverMethods(serverPkg.ImportPath, serverType, localPkg.ImportPath)
if err != nil {
return errgo.Mask(err)
}
arg := templateArg{
Imports: imports,
Methods: methods,
PkgName: localPkg.Name,
ClientType: clientType,
}
var buf bytes.Buffer
if err := code.Execute(&buf, arg); err != nil {
return errgo.Mask(err)
}
data, err := format.Source(buf.Bytes())
if err != nil {
return errgo.Notef(err, "cannot format source")
}
if err := writeOutput(data, clientType); err != nil {
return errgo.Mask(err)
}
return nil
}
func writeOutput(data []byte, clientType string) error {
filename := strings.ToLower(clientType) + "_generated.go"
if err := ioutil.WriteFile(filename, data, 0644); err != nil {
return errgo.Mask(err)
}
return nil
}
type method struct {
Name string
Doc string
ParamType string
RespType string
}
// serverMethods returns the list of server methods and required import packages
// provided by the given server type within the given server package.
//
// The localPkg package will be the one that the code will be generated in.
func serverMethods(serverPkg, serverType, localPkg string) ([]method, []string, error) {
cfg := packages.Config{
Mode: packages.LoadAllSyntax,
ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
return parser.ParseFile(fset, filename, src, parser.ParseComments)
},
}
pkgs, err := packages.Load(&cfg, serverPkg)
if err != nil {
return nil, nil, errgo.Notef(err, "cannot load %q", serverPkg)
}
if len(pkgs) != 1 {
return nil, nil, errgo.Newf("packages.Load returned %d packages, not 1", len(pkgs))
}
pkgInfo := pkgs[0]
pkg := pkgInfo.Types
obj := pkg.Scope().Lookup(serverType)
if obj == nil {
return nil, nil, errgo.Newf("type %s not found in %s", serverType, serverPkg)
}
objTypeName, ok := obj.(*types.TypeName)
if !ok {
return nil, nil, errgo.Newf("%s is not a type", serverType)
}
// Use the pointer type to get as many methods as possible.
ptrObjType := types.NewPointer(objTypeName.Type())
imports := map[string]string{
"gopkg.in/httprequest.v1": "httprequest",
"context": "context",
localPkg: "",
}
var methods []method
mset := types.NewMethodSet(ptrObjType)
for i := 0; i < mset.Len(); i++ {
sel := mset.At(i)
if !sel.Obj().Exported() {
continue
}
name := sel.Obj().Name()
if name == "Close" {
continue
}
ptype, rtype, err := parseMethodType(sel.Type().(*types.Signature))
if err != nil {
fmt.Fprintf(os.Stderr, "ignoring method %s: %v\n", name, err)
continue
}
comment := docComment(pkgInfo, sel)
methods = append(methods, method{
Name: name,
Doc: comment,
ParamType: typeStr(ptype, imports),
RespType: typeStr(rtype, imports),
})
}
delete(imports, localPkg)
var allImports []string
for path := range imports {
allImports = append(allImports, path)
}
return methods, allImports, nil
}
// docComment returns the doc comment for the method referred to
// by the given selection.
func docComment(pkg *packages.Package, sel *types.Selection) string {
obj := sel.Obj()
tokFile := pkg.Fset.File(obj.Pos())
if tokFile == nil {
panic("no file found for method")
}
filename := tokFile.Name()
comment := ""
declFound := false
packages.Visit([]*packages.Package{pkg}, func(pkg *packages.Package) bool {
for _, f := range pkg.Syntax {
if tokFile := pkg.Fset.File(f.Pos()); tokFile == nil || tokFile.Name() != filename {
continue
}
// We've found the file we're looking for. Now traverse all
// top level declarations looking for the right function declaration.
for _, decl := range f.Decls {
fdecl, ok := decl.(*ast.FuncDecl)
if ok && fdecl.Name.Pos() == obj.Pos() {
// Found it!
comment = commentStr(fdecl.Doc)
declFound = true
return false
}
}
}
return true
}, nil)
if !declFound {
panic(fmt.Sprintf("method declaration not found"))
}
return comment
}
func commentStr(c *ast.CommentGroup) string {
if c == nil {
return ""
}
var b []byte
for i, cc := range c.List {
if i > 0 {
b = append(b, '\n')
}
b = append(b, cc.Text...)
}
return string(b)
}
// typeStr returns the type string to be used when using the
// given type. It adds any needed import paths to the given
// imports map (map from package path to package id).
func typeStr(t types.Type, imports map[string]string) string {
if t == nil {
return ""
}
qualify := func(pkg *types.Package) string {
if name, ok := imports[pkg.Path()]; ok {
return name
}
name := pkg.Name()
// Make sure we're not duplicating the name.
// TODO if we are, make a new non-duplicated version.
for oldPkg, oldName := range imports {
if oldName == name {
panic(errgo.Newf("duplicate package name %s vs %s", pkg.Path(), oldPkg))
}
}
imports[pkg.Path()] = name
return name
}
return types.TypeString(t, qualify)
}
func parseMethodType(t *types.Signature) (ptype, rtype types.Type, err error) {
mp := t.Params()
if mp.Len() != 1 && mp.Len() != 2 {
return nil, nil, errgo.New("wrong argument count")
}
ptype0 := mp.At(mp.Len() - 1).Type()
ptype1, ok := ptype0.(*types.Pointer)
if !ok {
return nil, nil, errgo.New("parameter is not a pointer")
}
ptype = ptype1.Elem()
if _, ok := ptype.Underlying().(*types.Struct); !ok {
return nil, nil, errgo.Newf("parameter is %s, not a pointer to struct", ptype1.Elem())
}
rp := t.Results()
if rp.Len() > 2 {
return nil, nil, errgo.New("wrong result count")
}
if rp.Len() == 2 {
rtype = rp.At(0).Type()
}
return ptype, rtype, nil
}
httprequest-1.2.1/context_test.go 0000664 0000000 0000000 00000001460 13654526166 0017174 0 ustar 00root root 0000000 0000000 // Copyright 2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package httprequest_test
import (
"net/http"
"net/http/httptest"
"testing"
qt "github.com/frankban/quicktest"
"github.com/julienschmidt/httprouter"
"gopkg.in/httprequest.v1"
)
type testRequest struct {
httprequest.Route `httprequest:"GET /foo"`
}
func TestContextCancelledWhenDone(t *testing.T) {
c := qt.New(t)
var ch <-chan struct{}
hnd := testServer.Handle(func(p httprequest.Params, req *testRequest) {
ch = p.Context.Done()
})
router := httprouter.New()
router.Handle(hnd.Method, hnd.Path, hnd.Handle)
srv := httptest.NewServer(router)
_, err := http.Get(srv.URL + "/foo")
c.Assert(err, qt.Equals, nil)
select {
case <-ch:
default:
c.Fatal("context not canceled at end of handler.")
}
}
httprequest-1.2.1/error.go 0000664 0000000 0000000 00000006452 13654526166 0015610 0 ustar 00root root 0000000 0000000 package httprequest
import (
"context"
"encoding/json"
"fmt"
"net/http"
errgo "gopkg.in/errgo.v1"
)
// These constants are recognized by DefaultErrorMapper
// as mapping to the similarly named HTTP status codes.
const (
CodeBadRequest = "bad request"
CodeUnauthorized = "unauthorized"
CodeForbidden = "forbidden"
CodeNotFound = "not found"
)
// DefaultErrorUnmarshaler is the default error unmarshaler
// used by Client.
var DefaultErrorUnmarshaler = ErrorUnmarshaler(new(RemoteError))
// DefaultErrorMapper is used by Server when ErrorMapper is nil. It maps
// all errors to RemoteError instances; if an error implements the
// ErrorCoder interface, the Code field will be set accordingly; some
// codes will map to specific HTTP status codes (for example, if
// ErrorCode returns CodeBadRequest, the resulting HTTP status will be
// http.StatusBadRequest).
var DefaultErrorMapper = defaultErrorMapper
func defaultErrorMapper(ctx context.Context, err error) (status int, body interface{}) {
errorBody := errorResponseBody(err)
switch errorBody.Code {
case CodeBadRequest:
status = http.StatusBadRequest
case CodeUnauthorized:
status = http.StatusUnauthorized
case CodeForbidden:
status = http.StatusForbidden
case CodeNotFound:
status = http.StatusNotFound
default:
status = http.StatusInternalServerError
}
return status, errorBody
}
// errorResponse returns an appropriate error
// response for the provided error.
func errorResponseBody(err error) *RemoteError {
var errResp RemoteError
cause := errgo.Cause(err)
if cause, ok := cause.(*RemoteError); ok {
// It's a RemoteError already; Preserve the wrapped
// error message but copy everything else.
errResp = *cause
errResp.Message = err.Error()
return &errResp
}
// It's not a RemoteError. Preserve as much info as we can find.
errResp.Message = err.Error()
if coder, ok := cause.(ErrorCoder); ok {
errResp.Code = coder.ErrorCode()
}
return &errResp
}
// ErrorCoder may be implemented by an error to cause
// it to return a particular RemoteError code when
// DefaultErrorMapper is used.
type ErrorCoder interface {
ErrorCode() string
}
// RemoteError holds the default type of a remote error
// used by Client when no custom error unmarshaler
// is set. This type is also used by DefaultErrorMapper
// to marshal errors in Server.
type RemoteError struct {
// Message holds the error message.
Message string
// Code may hold a code that classifies the error.
Code string `json:",omitempty"`
// Info holds any other information associated with the error.
Info *json.RawMessage `json:",omitempty"`
}
// Error implements the error interface.
func (e *RemoteError) Error() string {
if e.Message == "" {
return "httprequest: no error message found"
}
return e.Message
}
// ErrorCode implements ErrorCoder by returning e.Code.
func (e *RemoteError) ErrorCode() string {
return e.Code
}
// Errorf returns a new RemoteError instance that uses the
// given code and formats the message with fmt.Sprintf(f, a...).
// If f is empty and there are no other arguments, code will also
// be used for the message.
func Errorf(code string, f string, a ...interface{}) *RemoteError {
var msg string
if f == "" && len(a) == 0 {
msg = code
} else {
msg = fmt.Sprintf(f, a...)
}
return &RemoteError{
Code: code,
Message: msg,
}
}
httprequest-1.2.1/example_handlers_test.go 0000664 0000000 0000000 00000002207 13654526166 0021023 0 ustar 00root root 0000000 0000000 package httprequest_test
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"github.com/julienschmidt/httprouter"
"gopkg.in/httprequest.v1"
)
type arithHandler struct {
}
type number struct {
N int
}
func (arithHandler) Add(arg *struct {
httprequest.Route `httprequest:"GET /:A/add/:B"`
A int `httprequest:",path"`
B int `httprequest:",path"`
}) (number, error) {
return number{
N: arg.A + arg.B,
}, nil
}
func ExampleServer_Handlers() {
f := func(p httprequest.Params) (arithHandler, context.Context, error) {
fmt.Printf("handle %s %s\n", p.Request.Method, p.Request.URL)
return arithHandler{}, p.Context, nil
}
router := httprouter.New()
var reqSrv httprequest.Server
for _, h := range reqSrv.Handlers(f) {
router.Handle(h.Method, h.Path, h.Handle)
}
srv := httptest.NewServer(router)
resp, err := http.Get(srv.URL + "/123/add/11")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
panic("status " + resp.Status)
}
fmt.Println("result:")
io.Copy(os.Stdout, resp.Body)
// Output: handle GET /123/add/11
// result:
// {"N":134}
}
httprequest-1.2.1/export_test.go 0000664 0000000 0000000 00000000130 13654526166 0017022 0 ustar 00root root 0000000 0000000 package httprequest
var AppendURL = appendURL
var MaxErrorBodySize = &maxErrorBodySize
httprequest-1.2.1/fancyerror.go 0000664 0000000 0000000 00000017626 13654526166 0016636 0 ustar 00root root 0000000 0000000 package httprequest
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"strings"
"unicode"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
"gopkg.in/errgo.v1"
)
func isDecodeResponseError(err error) bool {
_, ok := err.(*DecodeResponseError)
return ok
}
// DecodeResponseError represents an error when an HTTP
// response could not be decoded.
type DecodeResponseError struct {
// Response holds the problematic HTTP response.
// The body of this does not need to be closed
// and may be truncated if the response is large.
Response *http.Response
// DecodeError holds the error that was encountered
// when decoding.
DecodeError error
}
func (e *DecodeResponseError) Error() string {
return e.DecodeError.Error()
}
// newDecodeResponseError returns a new DecodeResponseError that
// uses the given error for its message. The Response field
// holds a copy of req. If bodyData is non-nil, it
// will be used as the data in the Response.Body field;
// otherwise body data will be read from req.Body.
func newDecodeResponseError(resp *http.Response, bodyData []byte, err error) *DecodeResponseError {
if bodyData == nil {
bodyData = readBodyForError(resp.Body)
}
resp1 := *resp
resp1.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
return &DecodeResponseError{
Response: &resp1,
DecodeError: errgo.Mask(err, errgo.Any),
}
}
// newDecodeRequestError returns a new DecodeRequestError that
// uses the given error for its message. The Request field
// holds a copy of req. If bodyData is non-nil, it
// will be used as the data in the Request.Body field;
// otherwise body data will be read from req.Body.
func newDecodeRequestError(req *http.Request, bodyData []byte, err error) *DecodeRequestError {
if bodyData == nil {
bodyData = readBodyForError(req.Body)
}
req1 := *req
req1.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
return &DecodeRequestError{
Request: &req1,
DecodeError: errgo.Mask(err, errgo.Any),
}
}
// DecodeRequestError represents an error when an HTTP
// request could not be decoded.
type DecodeRequestError struct {
// Request holds the problematic HTTP request.
// The body of this does not need to be closed
// and may be truncated if the response is large.
Request *http.Request
// DecodeError holds the error that was encountered
// when decoding.
DecodeError error
}
func (e *DecodeRequestError) Error() string {
return e.DecodeError.Error()
}
// fancyDecodeError is an error type that tries to
// produce a nice error message when the content
// type of a request or response is wrong.
type fancyDecodeError struct {
// contentType holds the contentType of the request or response.
contentType string
// body holds up to maxErrorBodySize saved bytes of the
// request or response body.
body []byte
}
func newFancyDecodeError(h http.Header, body io.Reader) *fancyDecodeError {
return &fancyDecodeError{
contentType: h.Get("Content-Type"),
body: readBodyForError(body),
}
}
func readBodyForError(r io.Reader) []byte {
data, _ := ioutil.ReadAll(io.LimitReader(noErrorReader{r}, int64(maxErrorBodySize)))
return data
}
// maxErrorBodySize holds the maximum amount of body that
// we try to read for an error before extracting text from it.
// It's reasonably large because:
// a) HTML often has large embedded scripts which we want
// to skip and
// b) it should be an relatively unusual case so the size
// shouldn't harm.
//
// It's defined as a variable so that it can be redefined in tests.
var maxErrorBodySize = 200 * 1024
// isJSONMediaType reports whether the content type of the given header implies
// that the content is JSON.
func isJSONMediaType(header http.Header) bool {
contentType := header.Get("Content-Type")
mediaType, _, _ := mime.ParseMediaType(contentType)
m := strings.TrimPrefix(mediaType, "application/")
if len(m) == len(mediaType) {
return false
}
// Look for +json suffix. See https://tools.ietf.org/html/rfc6838#section-4.2.8
// We recognize multiple suffixes too (e.g. application/something+json+other)
// as that seems to be a possibility.
for {
i := strings.Index(m, "+")
if i == -1 {
return m == "json"
}
if m[0:i] == "json" {
return true
}
m = m[i+1:]
}
}
// Error implements error.Error by trying to produce a decent
// error message derived from the body content.
func (e *fancyDecodeError) Error() string {
mediaType, _, err := mime.ParseMediaType(e.contentType)
if err != nil {
// Even if there's no media type, we want to see something useful.
mediaType = fmt.Sprintf("%q", e.contentType)
}
// TODO use charset.NewReader to convert from non-utf8 content?
switch mediaType {
case "text/html":
text, err := htmlToText(bytes.NewReader(e.body))
if err != nil {
// Note: it seems that this can never actually
// happen - the only way that the HTML parser
// can fail is if there's a read error and we've
// removed that possibility by using
// noErrorReader above.
return fmt.Sprintf("unexpected (and invalid) content text/html; want application/json; content: %q", sizeLimit(e.body))
}
if len(text) == 0 {
return fmt.Sprintf(`unexpected content type text/html; want application/json; content: %q`, sizeLimit(e.body))
}
return fmt.Sprintf(`unexpected content type text/html; want application/json; content: %s`, sizeLimit(text))
case "text/plain":
return fmt.Sprintf(`unexpected content type text/plain; want application/json; content: %s`, sizeLimit(sanitizeText(string(e.body), true)))
default:
return fmt.Sprintf(`unexpected content type %s; want application/json; content: %q`, mediaType, sizeLimit(e.body))
}
}
// noErrorReader wraps a reader, turning any errors into io.EOF
// so that we can extract some content even if we get an io error.
type noErrorReader struct {
r io.Reader
}
func (r noErrorReader) Read(buf []byte) (int, error) {
n, err := r.r.Read(buf)
if err != nil {
err = io.EOF
}
return n, err
}
func sizeLimit(data []byte) []byte {
const max = 1024
if len(data) < max {
return data
}
return append(data[0:max], fmt.Sprintf(" ... [%d bytes omitted]", len(data)-max)...)
}
// htmlToText attempts to return some relevant textual content
// from the HTML content in the given reader, formatted
// as a single line.
func htmlToText(r io.Reader) ([]byte, error) {
n, err := html.Parse(r)
if err != nil {
return nil, err
}
var buf bytes.Buffer
htmlNodeToText(&buf, n)
return buf.Bytes(), nil
}
// htmlNodeToText tries to extract some text from an arbitrary HTML
// page. It doesn't try to avoid looking in the header, because the
// title is in the header and is often the most succinct description of
// the page.
func htmlNodeToText(w *bytes.Buffer, n *html.Node) {
for ; n != nil; n = n.NextSibling {
switch n.Type {
case html.TextNode:
data := sanitizeText(n.Data, false)
if len(data) == 0 {
break
}
if w.Len() > 0 {
w.WriteString("; ")
}
w.Write(data)
case html.ElementNode:
if n.DataAtom != atom.Script {
htmlNodeToText(w, n.FirstChild)
}
case html.DocumentNode:
htmlNodeToText(w, n.FirstChild)
}
}
}
// sanitizeText tries to make the given string easier to read when presented
// as a single line. It squashes each run of white space into a single
// space, trims leading and trailing white space and trailing full
// stops. If newlineSemi is true, any newlines will be replaced with a
// semicolon.
func sanitizeText(s string, newlineSemi bool) []byte {
out := make([]byte, 0, len(s))
prevWhite := false
for _, r := range s {
if newlineSemi && r == '\n' && len(out) > 0 {
out = append(out, ';')
prevWhite = true
continue
}
if unicode.IsSpace(r) {
if len(out) > 0 {
prevWhite = true
}
continue
}
if prevWhite {
out = append(out, ' ')
prevWhite = false
}
out = append(out, string(r)...)
}
// Remove final space, any full stops and any final semicolon
// we might have added.
out = bytes.TrimRightFunc(out, func(r rune) bool {
return r == '.' || r == ' ' || r == ';'
})
return out
}
httprequest-1.2.1/fancyerror_test.go 0000664 0000000 0000000 00000036644 13654526166 0017676 0 ustar 00root root 0000000 0000000 package httprequest
import (
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
var fancyDecodeErrorTests = []struct {
about string
contentType string
body string
expectError string
}{{
about: "plain text",
contentType: "text/plain; charset=UTF-8",
body: " some\n text\t\n",
expectError: `unexpected content type text/plain; want application/json; content: some; text`,
}, {
about: "plain text with leading newline",
contentType: "text/plain; charset=UTF-8",
body: "\nsome text",
expectError: `unexpected content type text/plain; want application/json; content: some text`,
}, {
about: "unknown content type",
contentType: "something",
body: "some \nstuff",
expectError: `unexpected content type something; want application/json; content: "some \\nstuff"`,
}, {
about: "bad content type",
contentType: "/; charset=foo",
body: `some stuff`,
expectError: `unexpected content type "/; charset=foo"; want application/json; content: "some stuff"`,
}, {
about: "large text body",
contentType: "text/plain",
body: strings.Repeat("x", 1024+300),
expectError: `unexpected content type text/plain; want application/json; content: ` + strings.Repeat("x", 1024) + ` \.\.\. \[300 bytes omitted]`,
}, {
about: "html with no text",
contentType: "text/html",
body: "\n",
expectError: `unexpected content type text/html; want application/json; content: "\\n"`,
}, {
about: "non-utf8 text",
contentType: "text/plain; charset=iso8859-1",
body: "Pepp\xe9\n",
// It would be nice to make this better, but we don't
// really want to drag in all the charsets for this.
expectError: "unexpected content type text/plain; want application/json; content: Pepp\uFFFD",
}, {
about: "actual html error message from proxy",
contentType: "text/html; charset=UTF-8",
body: `
502 Proxy Error
Proxy Error
The proxy server received an invalid
response from an upstream server.
The proxy server could not handle the request GET /identity/v1/wait.
Reason: Error reading from remote server
Apache/2.4.7 (Ubuntu) Server at api.jujucharms.com Port 443
`,
expectError: `unexpected content type text/html; want application/json; content: 502 Proxy Error; Proxy Error; The proxy server received an invalid response from an upstream server; The proxy server could not handle the request; GET /identity/v1/wait; Reason:; Error reading from remote server; Apache/2\.4\.7 \(Ubuntu\) Server at api.jujucharms.com Port 443`,
}, {
about: "actual html error message web page",
contentType: "text/html; charset=UTF-8",
body: `
Page not found | Juju