http

package
v0.40.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2022 License: BSD-3-Clause Imports: 32 Imported by: 6

Documentation

Overview

Package http implement custom HTTP server with memory file system and simplified routing handler.

Problems

There are two problems that this library try to handle. First, optimizing serving local file system; second, complexity of routing regarding to their method, request type, and response type.

Assuming that we want to serve file system and API using ServeMux, the simplest registered handler are,

mux.HandleFunc("/", handleFileSystem)
mux.HandleFunc("/api", handleAPI)

The first problem is regarding to "http.ServeFile". Everytime the request hit "handleFileSystem" the "http.ServeFile" try to locate the file regarding to request path in system, read the content of file, parse its content type, and finally write the content-type, content-length, and body as response. This is time consuming. Of course, on modern OS, they may caching readed file descriptor in memory to minimize disk lookup, so the next call to the same file path may not touch the hard storage back again.

The second problem is regarding to handling API. We must check the request method, checking content-type, parsing query parameter or POST form in every sub-handler of API. Assume that we have an API with method POST and query parameter, the method to handle it would be like these,

handleAPILogin(res, req) {
	// (1) Check if method is POST
	// (2) Parse query parameter
	// (3) Process request
	// (4) Write response
}

The step number 1, 2, 4 needs to be written for every handler of our API.

Solutions

The solution to the first problem is by mapping all content of files to be served into memory. This cause more memory to be consumed on server side but we minimize path lookup, and cache-miss on OS level.

Serving file system is handled by package memfs, which can be set on ServerOptions. For example, to serve all content in directory "www", we can set the ServerOptions to,

opts := &http.ServerOptions{
	Options: memfs.Options{
		Root: "www",
	},
	Address: ":8080",
}
httpServer, err := NewServer(opts)

There is a limit on size of file to be mapped on memory. See the package "lib/memfs" for more information.

The solution to the second problem is by mapping the registered request per method and by path. User just need to focus on step 3, handling on how to process request, all of process on step 1, 2, and 4 will be handled by our library.

import (
	libhttp "github.com/shuLhan/share/lib/http"
)

...

epAPILogin := &libhttp.Endpoint{
	Method: libhttp.RequestMethodPost,
	Path: "/api/login",
	RequestType: libhttp.RequestTypeQuery,
	ResponseType: libhttp.ResponseTypeJSON,
	Call: handleLogin,
}
server.RegisterEndpoint(epAPILogin)

...

Upon receiving request to "POST /api/login", the library will call "HttpRequest.ParseForm()", read the content of body and pass them to "handleLogin",

func handleLogin(epr *EndpointRequest) (resBody []byte, err error) {
	// Process login input from epr.HttpRequest.Form,
	// epr.HttpRequest.PostForm, and/or epr.RequestBody.
	// Return response body and error.
}

Routing

The Endpoint allow binding the unique key into path using colon ":" as the first character.

For example, after registering the following Endpoint,

epBinding := &libhttp.Endpoint{
	Method: libhttp.RequestMethodGet,
	Path: "/category/:name",
	RequestType: libhttp.RequestTypeQuery,
	ResponseType: libhttp.ResponseTypeJSON,
	Call: handleCategory,
}
server.RegisterEndpoint(epBinding)

when the server receiving GET request using path "/category/book?limit=10", it will put the "book" and "10" into http.Request's Form with key is "name" and "limit"

fmt.Println("request.Form:", req.Form)
// request.Form: map[name:[book] limit:[10]]

The key binding must be unique between path and query. If query has the same key then it will be overridden by value in path. For example, using the above endpoint, request with "/category/book?name=Hitchiker" will result in Request.Form:

map[name:[book]]

not

map[name:[book Hitchiker]]

Callback error handling

Each Endpoint can have their own error handler. If its nil, it will default to DefaultErrorHandler, which return the error as JSON with the following format,

{"code":<HTTP_STATUS_CODE>,"message":<err.Error()>}

Summary

The pseudocode below illustrate how Endpoint, Callback, and CallbackErrorHandler works when the Server receive HTTP request,

func (server *Server) (w http.ResponseWriter, req *http.Request) {
	for _, endpoint := range server.endpoints {
		if endpoint.Method.String() != req.Method {
			continue
		}

		epr := &EndpointRequest{
			Endpoint: endpoint,
			HttpWriter: w,
			HttpRequest: req,
		}
		epr.RequestBody, _ = io.ReadAll(req.Body)

		// Check request type, and call ParseForm or
		// ParseMultipartForm if required.

		var resBody []byte
		resBody, epr.Error = endpoint.Call(epr)
		if err != nil {
			endpoint.ErrorHandler(epr)
			return
		}
		// Set content-type based on endpoint.ResponseType,
		// and write the response body,
		w.Write(resBody)
		return
	}
	// If request is HTTP GET, check if Path exist as static
	// contents in Memfs.
}

Known Bugs and Limitations

* The server does not handle CONNECT method

* Missing test for request with content-type multipart-form

* We can not register path with ambigous route. For example, "/:x" and "/y" are ambiguous because one is dynamic path using key binding "x" and the last one is static path to "y".

Index

Examples

Constants

View Source
const (
	ContentEncodingBzip2    = "bzip2"
	ContentEncodingCompress = "compress" // Using LZW.
	ContentEncodingGzip     = "gzip"
	ContentEncodingDeflate  = "deflate" // Using zlib.

	ContentTypeBinary        = "application/octet-stream"
	ContentTypeForm          = "application/x-www-form-urlencoded"
	ContentTypeMultipartForm = "multipart/form-data"
	ContentTypeHTML          = "text/html; charset=utf-8"
	ContentTypeJSON          = "application/json"
	ContentTypePlain         = "text/plain; charset=utf-8"
	ContentTypeXML           = "text/xml; charset=utf-8"

	HeaderACAllowCredentials = "Access-Control-Allow-Credentials"
	HeaderACAllowHeaders     = "Access-Control-Allow-Headers"
	HeaderACAllowMethod      = "Access-Control-Allow-Method"
	HeaderACAllowOrigin      = "Access-Control-Allow-Origin"
	HeaderACExposeHeaders    = "Access-Control-Expose-Headers"
	HeaderACMaxAge           = "Access-Control-Max-Age"
	HeaderACRequestHeaders   = "Access-Control-Request-Headers"
	HeaderACRequestMethod    = "Access-Control-Request-Method"
	HeaderAcceptEncoding     = "Accept-Encoding"
	HeaderAllow              = "Allow"
	HeaderAuthKeyBearer      = "Bearer"
	HeaderAuthorization      = "Authorization"
	HeaderContentEncoding    = "Content-Encoding"
	HeaderContentLength      = "Content-Length"
	HeaderContentType        = "Content-Type"
	HeaderCookie             = "Cookie"
	HeaderETag               = "ETag"
	HeaderHost               = "Host"
	HeaderIfNoneMatch        = "If-None-Match"
	HeaderLocation           = "Location"
	HeaderOrigin             = "Origin"
	HeaderUserAgent          = "User-Agent"
	HeaderXForwardedFor      = "X-Forwarded-For" // https://en.wikipedia.org/wiki/X-Forwarded-For
	HeaderXRealIp            = "X-Real-Ip"
)

List of known HTTP header keys and values.

Variables

View Source
var (
	ErrClientDownloadNoOutput = errors.New("invalid or empty client download output")

	//
	// ErrEndpointAmbiguous define an error when registering path that
	// already exist.  For example, after registering "/:x", registering
	// "/:y" or "/z" on the same HTTP method will result in ambiguous.
	//
	ErrEndpointAmbiguous = errors.New("ambigous endpoint")

	//
	// ErrEndpointKeyDuplicate define an error when registering path with
	// the same keys, for example "/:x/:x".
	//
	ErrEndpointKeyDuplicate = errors.New("duplicate key in route")

	//
	// ErrEndpointKeyEmpty define an error when path contains an empty
	// key, for example "/:/y".
	//
	ErrEndpointKeyEmpty = errors.New("empty route's key")
)

Functions

func DefaultErrorHandler added in v0.21.0

func DefaultErrorHandler(epr *EndpointRequest)

DefaultErrorHandler define the default function that will called to handle the error returned from Callback function, if the Endpoint.ErrorHandler is not defined.

First, it will check if error instance of *errors.E. If its true, it will use the Code value for HTTP status code, otherwise if its zero or invalid, it will set to http.StatusInternalServerError.

Second, it will set the HTTP header Content-Type to "application/json" and write the response body as JSON format,

{"code":<HTTP_STATUS_CODE>, "message":<err.Error()>}

func IPAddressOfRequest added in v0.24.0

func IPAddressOfRequest(headers http.Header, defAddr string) (addr string)

IPAddressOfRequest get the client IP address from HTTP request header "X-Real-IP" or "X-Forwarded-For", which ever non-empty first. If no headers present, use the default address.

Example
defAddress := "192.168.100.1"

headers := http.Header{
	"X-Real-Ip": []string{"127.0.0.1"},
}
fmt.Println("Request with X-Real-IP:", IPAddressOfRequest(headers, defAddress))

headers = http.Header{
	"X-Forwarded-For": []string{"127.0.0.2, 192.168.100.1"},
}
fmt.Println("Request with X-Forwarded-For:", IPAddressOfRequest(headers, defAddress))

headers = http.Header{}
fmt.Println("Request without X-* headers:", IPAddressOfRequest(headers, defAddress))
Output:

Request with X-Real-IP: 127.0.0.1
Request with X-Forwarded-For: 127.0.0.2
Request without X-* headers: 192.168.100.1

func ParseResponseHeader added in v0.5.0

func ParseResponseHeader(raw []byte) (resp *http.Response, rest []byte, err error)

ParseResponseHeader parse HTTP response header and return it as standard HTTP Response with unreaded packet.

func ParseXForwardedFor added in v0.24.0

func ParseXForwardedFor(val string) (clientAddr string, proxyAddrs []string)

ParseXForwardedFor parse the HTTP header "X-Forwarded-For" value from the following format "client, proxy1, proxy2" into client address and list of proxy addressess.

Example
values := []string{
	"",
	"203.0.113.195",
	"203.0.113.195, 70.41.3.18, 150.172.238.178",
	"2001:db8:85a3:8d3:1319:8a2e:370:7348",
}
for _, val := range values {
	clientAddr, proxyAddrs := ParseXForwardedFor(val)
	fmt.Println(clientAddr, proxyAddrs)
}
Output:

[]
203.0.113.195 []
203.0.113.195 [70.41.3.18 150.172.238.178]
2001:db8:85a3:8d3:1319:8a2e:370:7348 []

func UnmarshalForm added in v0.39.0

func UnmarshalForm(in url.Values, out interface{}) (err error)

UnmarshalForm read struct fields tagged with `form:` from out as key and set its using the value from url.Values based on that key. If the field does not have `form:` tag but it is exported, then it will use the field name, in case insensitive.

Only the following types are supported: bool, int/intX, uint/uintX, floatX, string, []byte, or type that implement BinaryUnmarshaler (UnmarshalBinary), json.Unmarshaler (UnmarshalJSON), or TextUnmarshaler (UnmarshalText).

A bool type can be set to true using the following string value: "true", "yes", or "1".

If the input contains multiple values but the field type is not slice, the field will be set using the first value.

It will return an error if the out variable is not set-able (the type is not a pointer to a struct). It will not return an error if one of the input value is not match with field type.

Example
type T struct {
	Rat    *big.Rat `form:"big.Rat"`
	String string   `form:"string"`
	Bytes  []byte   `form:"bytes"`
	Int    int      `form:""` // With empty tag.
	F64    float64  `form:"f64"`
	F32    float32  `form:"f32"`
	NotSet int16    `form:"notset"`
	Uint8  uint8    `form:" uint8 "`
	Bool   bool     // Without tag.
}
var (
	in = url.Values{}

	out    T
	ptrOut *T
	err    error
)

in.Set("big.Rat", "1.2345")
in.Set("string", "a_string")
in.Set("bytes", "bytes")
in.Set("int", "1")
in.Set("f64", "6.4")
in.Set("f32", "3.2")
in.Set("uint8", "2")
in.Set("bool", "true")

err = UnmarshalForm(in, &out)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Printf("%+v\n", out)
}

// Set the struct without initialized.
err = UnmarshalForm(in, &ptrOut)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Printf("%+v\n", ptrOut)
}
Output:

{Rat:2469/2000 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true}
&{Rat:2469/2000 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true}
Example (Error)
type T struct {
	Int int
}

var (
	in = url.Values{}

	out    T
	ptrOut *T
	err    error
)

// Passing out as unsetable by function.
err = UnmarshalForm(in, out)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Println(out)
}

// Passing out as un-initialized pointer.
err = UnmarshalForm(in, ptrOut)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Println(out)
}

// Set the field with invalid type.
in.Set("int", "a")
err = UnmarshalForm(in, &out)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Println(out)
}
Output:

UnmarshalForm: expecting *T got http.T
UnmarshalForm: *http.T is not initialized
{0}
Example (Slice)
type SliceT struct {
	NotSlice    string   `form:"multi_value"`
	SliceString []string `form:"slice_string"`
	SliceInt    []int    `form:"slice_int"`
}

var (
	in = url.Values{}

	sliceOut    SliceT
	ptrSliceOut *SliceT
	err         error
)

in.Add("multi_value", "first")
in.Add("multi_value", "second")
in.Add("slice_string", "multi")
in.Add("slice_string", "value")
in.Add("slice_int", "123")
in.Add("slice_int", "456")

err = UnmarshalForm(in, &sliceOut)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Printf("%+v\n", sliceOut)
}

err = UnmarshalForm(in, &ptrSliceOut)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Printf("%+v\n", ptrSliceOut)
}
Output:

{NotSlice:first SliceString:[multi value] SliceInt:[123 456]}
&{NotSlice:first SliceString:[multi value] SliceInt:[123 456]}
Example (Zero)
type T struct {
	Rat    *big.Rat `form:"big.Rat"`
	String string   `form:"string"`
	Bytes  []byte   `form:"bytes"`
	Int    int      `form:""` // With empty tag.
	F64    float64  `form:"f64"`
	F32    float32  `form:"f32"`
	NotSet int16    `form:"notset"`
	Uint8  uint8    `form:" uint8 "`
	Bool   bool     // Without tag.
}
var (
	in = url.Values{}

	out T
	err error
)

in.Set("big.Rat", "1.2345")
in.Set("string", "a_string")
in.Set("bytes", "bytes")
in.Set("int", "1")
in.Set("f64", "6.4")
in.Set("f32", "3.2")
in.Set("uint8", "2")
in.Set("bool", "true")

err = UnmarshalForm(in, &out)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Printf("%+v\n", out)
}

in.Set("bool", "")
in.Set("int", "")
in.Set("uint8", "")
in.Set("f32", "")
in.Set("f64", "")
in.Set("string", "")
in.Set("bytes", "")
in.Set("big.Rat", "")

err = UnmarshalForm(in, &out)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Printf("%+v\n", out)
}
Output:

{Rat:2469/2000 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true}
{Rat:0/1 String: Bytes:[] Int:0 F64:0 F32:0 NotSet:0 Uint8:0 Bool:false}

Types

type CORSOptions added in v0.24.0

type CORSOptions struct {

	// AllowOrigins contains global list of cross-site Origin that are
	// allowed during preflight requests by the OPTIONS method.
	// The list is case-sensitive.
	// To allow all Origin, one must add "*" string to the list.
	AllowOrigins []string

	// AllowHeaders contains global list of allowed headers during
	// preflight requests by the OPTIONS method.
	// The list is case-insensitive.
	// To allow all headers, one must add "*" string to the list.
	AllowHeaders []string

	// ExposeHeaders contains list of allowed headers.
	// This list will be send when browser request OPTIONS without
	// request-method.
	ExposeHeaders []string

	// MaxAge gives the value in seconds for how long the response to
	// the preflight request can be cached for without sending another
	// preflight request.
	MaxAge int

	// AllowCredentials indicates whether or not the actual request
	// can be made using credentials.
	AllowCredentials bool
	// contains filtered or unexported fields
}

CORSOptions define optional options for server to allow other servers to access its resources.

type Callback

type Callback func(req *EndpointRequest) (resBody []byte, err error)

Callback define a type of function for handling registered handler.

The function will have the query URL, request multipart form data, and request body ready to be used in EndpointRequest.HttpRequest and EndpointRequest.RequestBody fields.

The EndpointRequest.HttpWriter can be used to write custom header or to write cookies but should not be used to write response body.

The error return type should be an instance of *errors.E, with E.Code define the HTTP status code. If error is not nil and not *errors.E, server will response with internal-server-error status code.

type CallbackErrorHandler added in v0.21.0

type CallbackErrorHandler func(epr *EndpointRequest)

CallbackErrorHandler define the function that can be used to handle an error returned from Endpoint.Call. By default, if Endpoint.Call is nil, it will use DefaultErrorHandler.

type Client added in v0.15.0

type Client struct {
	*http.Client
	// contains filtered or unexported fields
}

Client is a wrapper for standard http.Client with simplified usabilities, including setting default headers, uncompressing response body.

func NewClient added in v0.15.0

func NewClient(opts *ClientOptions) (client *Client)

NewClient create and initialize new Client.

The client will have KeepAlive timeout set to 30 seconds, with 1 maximum idle connection, and 90 seconds IdleConnTimeout.

func (*Client) Delete added in v0.17.0

func (client *Client) Delete(path string, headers http.Header, params url.Values) (
	httpRes *http.Response, resBody []byte, err error,
)

Delete send the DELETE request to server using path and params as query parameters. On success, it will return the uncompressed response body.

func (*Client) Do added in v0.25.0

func (client *Client) Do(httpRequest *http.Request) (
	httpRes *http.Response, resBody []byte, err error,
)

Do overwrite the standard http Client.Do to allow debugging request and response, and to read and return the response body immediately.

func (*Client) Download added in v0.34.0

func (client *Client) Download(req DownloadRequest) (httpRes *http.Response, err error)

Download a resource from remote server and write it into DownloadRequest.Output.

If the DownloadRequest.Output is nil, it will return an error ErrClientDownloadNoOutput. If server return HTTP code beside 200, it will return non-nil http.Response with an error.

func (*Client) GenerateHttpRequest added in v0.25.0

func (client *Client) GenerateHttpRequest(
	method RequestMethod,
	path string,
	requestType RequestType,
	headers http.Header,
	params interface{},
) (httpRequest *http.Request, err error)

GenerateHttpRequest generate http.Request from method, path, requestType, headers, and params.

For HTTP method GET, CONNECT, DELETE, HEAD, OPTIONS, or TRACE; the params value should be nil or url.Values. If its url.Values, then the params will be encoded as query parameters.

For HTTP method is PATCH, POST, or PUT; the params will converted based on requestType rules below,

* If requestType is RequestTypeQuery and params is url.Values it will be added as query parameters in the path.

* If requestType is RequestTypeForm and params is url.Values it will be added as URL encoded in the body.

* If requestType is RequestTypeMultipartForm and params type is map[string][]byte, then it will be converted as multipart form in the body.

* If requestType is RequestTypeJSON and params is not nil, the params will be encoded as JSON in body.

func (*Client) Get added in v0.15.0

func (client *Client) Get(path string, headers http.Header, params url.Values) (
	httpRes *http.Response, resBody []byte, err error,
)

Get send the GET request to server using path and params as query parameters. On success, it will return the uncompressed response body.

func (*Client) Post added in v0.23.0

func (client *Client) Post(path string, headers http.Header, params url.Values) (
	httpRes *http.Response, resBody []byte, err error,
)

Post send the POST request to path without setting "Content-Type". If the params is not nil, it will send as query parameters in the path.

func (*Client) PostForm added in v0.15.0

func (client *Client) PostForm(path string, headers http.Header, params url.Values) (
	httpRes *http.Response, resBody []byte, err error,
)

PostForm send the POST request to path using "application/x-www-form-urlencoded".

func (*Client) PostFormData added in v0.15.0

func (client *Client) PostFormData(
	path string,
	headers http.Header,
	params map[string][]byte,
) (
	httpRes *http.Response, resBody []byte, err error,
)

PostFormData send the POST request to path with all parameters is send using "multipart/form-data".

func (*Client) PostJSON added in v0.15.0

func (client *Client) PostJSON(path string, headers http.Header, params interface{}) (
	httpRes *http.Response, resBody []byte, err error,
)

PostJSON send the POST request with content type set to "application/json" and params encoded automatically to JSON.

func (*Client) Put added in v0.18.0

func (client *Client) Put(path string, headers http.Header, body []byte) (
	*http.Response, []byte, error,
)

Put send the HTTP PUT request with specific content type and body to specific path at the server.

func (*Client) PutJSON added in v0.17.0

func (client *Client) PutJSON(path string, headers http.Header, params interface{}) (
	httpRes *http.Response, resBody []byte, err error,
)

PutJSON send the PUT request with content type set to "application/json" and params encoded automatically to JSON.

type ClientOptions added in v0.33.0

type ClientOptions struct {
	// Headers define default headers that will be send in any request to
	// server.
	// This field is optional.
	Headers http.Header

	// ServerUrl is any path that is static and will never changes during
	// request to server.
	// This field is required.
	ServerUrl string

	// Timeout affect the http Transport Timeout and TLSHandshakeTimeout.
	// This field is optional, if not set it will set to 10 seconds.
	Timeout time.Duration

	// AllowInsecure if its true, it will allow to connect to server with
	// unknown certificate authority.
	// This field is optional.
	AllowInsecure bool
}

ClientOptions options for HTTP client.

type ClientRequest added in v0.34.0

type ClientRequest struct {
	// Headers additional header to be send on request.
	// This field is optional.
	Headers http.Header

	//
	// Params define parameter to be send on request.
	// This field is optional.
	//
	// For Method GET, CONNECT, DELETE, HEAD, OPTIONS, or TRACE; the
	// params value should be nil or url.Values.
	// If its url.Values, then the params will be encoded as query
	// parameters.
	//
	// For Method PATCH, POST, or PUT; the Params will converted based on
	// Type rules below,
	//
	// * If Type is RequestTypeQuery and Params is url.Values it will be
	// added as query parameters in the Path.
	//
	// * If Type is RequestTypeForm and Params is url.Values it will be
	// added as URL encoded in the body.
	//
	// * If Type is RequestTypeMultipartForm and Params is
	// map[string][]byte, then it will be converted as multipart form in
	// the body.
	//
	// * If Type is RequestTypeJSON and Params is not nil, the params will
	// be encoded as JSON in body using json.Encode().
	//
	Params interface{}

	// The Path to resource on the server.
	// This field is required, if its empty default to "/".
	Path string

	// The HTTP method of request.
	// This field is optional, if its empty default to RequestMethodGet
	// (GET).
	Method RequestMethod

	// The Type of request.
	// This field is optional, it's affect how the Params field encoded in
	// the path or body.
	Type RequestType
}

ClientRequest define the parameters for each Client methods.

type DownloadRequest added in v0.34.0

type DownloadRequest struct {
	// Output define where the downloaded resource from server will be
	// writen.
	// This field is required.
	Output io.Writer

	ClientRequest
}

DownloadRequest define the parameter for Client's Download() method.

type Endpoint

type Endpoint struct {
	// ErrorHandler define the function that will handle the error
	// returned from Call.
	ErrorHandler CallbackErrorHandler

	// Eval define evaluator for route that will be called after global
	// evaluators and before callback.
	Eval Evaluator

	// Call is the main process of route.
	Call Callback

	// Path contains route to be served, default to "/" if its empty.
	Path string

	// RequestType contains type of request, default to RequestTypeNone.
	RequestType RequestType

	// ResponseType contains type of request, default to ResponseTypeNone.
	ResponseType ResponseType

	// Method contains HTTP method, default to GET.
	Method RequestMethod
}

Endpoint represent route that will be handled by server. Each route have their own evaluator that will be evaluated after global evaluators from server.

Example (ErrorHandler)
serverOpts := &ServerOptions{
	Address: "127.0.0.1:8123",
}
server, _ := NewServer(serverOpts)

endpointError := &Endpoint{
	Method:       RequestMethodGet,
	Path:         "/",
	RequestType:  RequestTypeQuery,
	ResponseType: ResponseTypePlain,
	Call: func(epr *EndpointRequest) ([]byte, error) {
		return nil, fmt.Errorf(epr.HttpRequest.Form.Get("error"))
	},
	ErrorHandler: func(epr *EndpointRequest) {
		epr.HttpWriter.Header().Set(HeaderContentType, ContentTypePlain)

		codeMsg := strings.Split(epr.Error.Error(), ":")
		if len(codeMsg) != 2 {
			epr.HttpWriter.WriteHeader(http.StatusInternalServerError)
			_, _ = epr.HttpWriter.Write([]byte(epr.Error.Error()))
		} else {
			code, _ := strconv.Atoi(codeMsg[0])
			epr.HttpWriter.WriteHeader(code)
			_, _ = epr.HttpWriter.Write([]byte(codeMsg[1]))
		}
	},
}
_ = server.RegisterEndpoint(endpointError)

go func() {
	_ = server.Start()
}()
defer func() {
	_ = server.Stop(1 * time.Second)
}()
time.Sleep(1 * time.Second)

clientOpts := &ClientOptions{
	ServerUrl: "http://" + serverOpts.Address,
}
client := NewClient(clientOpts)

params := url.Values{}
params.Set("error", "400:error with status code")
httpres, resbody, _ := client.Get("/", nil, params)
fmt.Printf("%d: %s\n", httpres.StatusCode, resbody)

params.Set("error", "error without status code")
httpres, resbody, _ = client.Get("/", nil, params)
fmt.Printf("%d: %s\n", httpres.StatusCode, resbody)
Output:

400: error with status code
500: error without status code

func (*Endpoint) HTTPMethod added in v0.10.1

func (ep *Endpoint) HTTPMethod() string

HTTPMethod return the string representation of HTTP method as predefined in "net/http".

type EndpointRequest added in v0.24.0

type EndpointRequest struct {
	HttpWriter  http.ResponseWriter
	Error       error
	Endpoint    *Endpoint
	HttpRequest *http.Request
	RequestBody []byte
}

EndpointRequest wrap the called Endpoint and common two parameters in HTTP handler: the http.ResponseWriter and http.Request.

The RequestBody field contains the full http.Request.Body that has been read.

The Error field is used by CallbackErrorHandler.

type EndpointResponse added in v0.24.0

type EndpointResponse struct {
	liberrors.E
	Data interface{} `json:"data,omitempty"`

	// The Limit field contains the maximum number of records per page.
	Limit int64 `json:"limit,omitempty"`

	// The Offset field contains the start index of paging.
	// If Page values is from request then the offset can be set to
	// Page times Limit.
	Offset int64 `json:"offset,omitempty"`

	// The Page field contains the requested or current page of response.
	Page int64 `json:"page,omitempty"`

	// Count field contains the total number of records in Data.
	Count int64 `json:"count,omitempty"`

	// Total field contains the total number of all records.
	Total int64 `json:"total,omitempty"`
}

EndpointResponse is one of the common HTTP response container that can be used by Server implementor. Its embed the lib/errors.E type to work seamlessly with Endpoint.Call handler for checking the returned error.

If the response is paging, contains more than one item in Data, one can set the current status of paging in field Limit, Offset, Page, and Count.

See the example below on how to use it with Endpoint.Call handler.

Example
type myData struct {
	ID string
}

server, err := NewServer(&ServerOptions{
	Address: "127.0.0.1:7016",
})
if err != nil {
	log.Fatal(err)
}

// Lest say we have an endpoint that echoing back the request
// parameter "id" back to client inside the EndpointResponse.Data using
// myData as JSON format.
// If the parameter "id" is missing or empty it will return an HTTP
// status code with message as defined in EndpointResponse.
err = server.RegisterEndpoint(&Endpoint{
	Method:       RequestMethodGet,
	RequestType:  RequestTypeQuery,
	ResponseType: ResponseTypeJSON,
	Call: func(epr *EndpointRequest) ([]byte, error) {
		res := &EndpointResponse{}
		id := epr.HttpRequest.Form.Get("id")
		if len(id) == 0 {
			res.Code = http.StatusBadRequest
			res.Message = "empty parameter id"
			return nil, res
		}
		if id == "0" {
			// If the EndpointResponse.Code is 0, it will
			// default to http.StatusInternalServerError
			res.Message = "id value 0 cause internal server error"
			return nil, res
		}
		res.Code = http.StatusOK
		res.Data = &myData{
			ID: id,
		}
		return json.Marshal(res)
	},
})
if err != nil {
	log.Fatal(err)
}

go func() {
	err := server.Start()
	if err != nil {
		log.Fatal(err)
	}
}()
time.Sleep(500)

clientOpts := &ClientOptions{
	ServerUrl: "http://127.0.0.1:7016",
}
cl := NewClient(clientOpts)
params := url.Values{}

// Test call endpoint without "id" parameter.
_, resBody, err := cl.Get("/", nil, params)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("GET / => %s\n", resBody)

// Test call endpoint with "id" parameter set to "0", it should return
// HTTP status 500 with custom message.
params.Set("id", "0")
_, resBody, err = cl.Get("/", nil, params)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("GET /?id=0 => %s\n", resBody)

// Test with "id" parameter is set.
params.Set("id", "1000")
_, resBody, err = cl.Get("/", nil, params)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("GET /?id=1000 => %s\n", resBody)
Output:

GET / => {"code":400,"message":"empty parameter id"}
GET /?id=0 => {"code":500,"message":"id value 0 cause internal server error"}
GET /?id=1000 => {"code":200,"data":{"ID":"1000"}}

func (*EndpointResponse) Unwrap added in v0.24.0

func (epr *EndpointResponse) Unwrap() (err error)

Unwrap return the error as instance of *liberror.E.

type Evaluator

type Evaluator func(req *http.Request, reqBody []byte) error

Evaluator evaluate the request. If request is invalid, the error will tell the response code and the error message to be written back to client.

type FSHandler added in v0.36.0

type FSHandler func(node *memfs.Node, res http.ResponseWriter, req *http.Request) bool

FSHandler define the function to inspect each GET request to Server MemFS instance. The node parameter contains the requested file inside the memfs.

If the handler return true, server will continue processing the node (writing the Node content type, body, and so on).

If the handler return false, server stop processing the node and return immediately, which means the function should have already handle writing the header, status code, and/or body.

type RequestMethod

type RequestMethod int

RequestMethod define type of HTTP method.

const (
	RequestMethodGet RequestMethod = iota
	RequestMethodConnect
	RequestMethodDelete
	RequestMethodHead
	RequestMethodOptions
	RequestMethodPatch
	RequestMethodPost
	RequestMethodPut
	RequestMethodTrace
)

List of known HTTP methods.

func (RequestMethod) String added in v0.18.0

func (rm RequestMethod) String() string

String return the string representation of request method.

type RequestType

type RequestType int

RequestType define type of HTTP request.

const (
	RequestTypeNone RequestType = iota
	RequestTypeQuery
	RequestTypeForm
	RequestTypeMultipartForm
	RequestTypeJSON
	RequestTypeXML
)

List of valid request type.

func (RequestType) String added in v0.25.0

func (rt RequestType) String() string

String return the string representation of request type as in "Content-Type" header. For RequestTypeNone or RequestTypeQuery it will return an empty string "".

type ResponseType

type ResponseType int

ResponseType define type for HTTP response.

const (
	ResponseTypeNone ResponseType = iota
	ResponseTypeBinary
	ResponseTypeHTML
	ResponseTypeJSON
	ResponseTypePlain
	ResponseTypeXML
)

List of valid response type.

func (ResponseType) String added in v0.25.0

func (restype ResponseType) String() string

String return the string representation of ResponseType as in "Content-Type" header. For ResponseTypeNone it will return an empty string "".

type Server

type Server struct {
	*http.Server

	// Options for server, set by calling NewServer.
	// This field is exported only for reference, for example logging in
	// the Options when server started.
	// Modifying the value of Options after server has been started may
	// cause undefined effects.
	Options *ServerOptions
	// contains filtered or unexported fields
}

Server define HTTP server.

Example (CustomHTTPStatusCode)
type CustomResponse struct {
	Status int `json:"status"`
}
exp := CustomResponse{
	Status: http.StatusBadRequest,
}

opts := &ServerOptions{
	Address: "127.0.0.1:8123",
}

testServer, err := NewServer(opts)
if err != nil {
	log.Fatal(err)
}

go func() {
	err = testServer.Start()
	if err != nil {
		log.Println(err)
	}
}()

defer func() {
	_ = testServer.Stop(5 * time.Second)
}()

epCustom := &Endpoint{
	Path:         "/error/custom",
	RequestType:  RequestTypeJSON,
	ResponseType: ResponseTypeJSON,
	Call: func(epr *EndpointRequest) (
		resbody []byte, err error,
	) {
		epr.HttpWriter.WriteHeader(exp.Status)
		return json.Marshal(exp)
	},
}

err = testServer.registerPost(epCustom)
if err != nil {
	log.Fatal(err)
}

// Wait for the server fully started.
time.Sleep(1 * time.Second)

clientOpts := &ClientOptions{
	ServerUrl: "http://127.0.0.1:8123",
}
client := NewClient(clientOpts)

httpRes, resBody, err := client.PostJSON(epCustom.Path, nil, nil)
if err != nil {
	log.Fatal(err)
}

fmt.Printf("%d\n", httpRes.StatusCode)
fmt.Printf("%s\n", resBody)
Output:

400
{"status":400}

func NewServer

func NewServer(opts *ServerOptions) (srv *Server, err error)

NewServer create and initialize new HTTP server that serve root directory with custom connection.

func (*Server) HandleFS added in v0.24.0

func (srv *Server) HandleFS(res http.ResponseWriter, req *http.Request)

HandleFS handle the request as resource in the memory file system. This method only works if the Server.Options.Memfs is not nil.

If the request Path exists and Server Options FSHandler is set and returning false, it will return immediately.

If the request Path exists in file system, it will return 200 OK with the header Content-Type set accordingly to the detected file type and the response body set to the content of file. If the request Method is HEAD, only the header will be sent back to client.

If the request Path is not exist it will return 404 Not Found.

func (*Server) RedirectTemp

func (srv *Server) RedirectTemp(res http.ResponseWriter, redirectURL string)

RedirectTemp make the request to temporary redirect (307) to new URL.

func (*Server) RegisterEndpoint added in v0.14.0

func (srv *Server) RegisterEndpoint(ep *Endpoint) (err error)

RegisterEndpoint register the Endpoint based on Method. If Method field is not set, it will default to GET. The Endpoint.Call field MUST be set, or it will return an error.

Endpoint with method HEAD and OPTIONS does not have any effect because it already handled automatically by server.

Endpoint with method CONNECT and TRACE will return an error because its not supported yet.

func (*Server) RegisterEvaluator

func (srv *Server) RegisterEvaluator(eval Evaluator)

RegisterEvaluator register HTTP middleware that will be called before Endpoint evalutor and callback is called.

func (*Server) ServeHTTP

func (srv *Server) ServeHTTP(res http.ResponseWriter, req *http.Request)

ServeHTTP handle mapping of client request to registered endpoints.

func (*Server) Start

func (srv *Server) Start() (err error)

Start the HTTP server.

func (*Server) Stop added in v0.17.0

func (srv *Server) Stop(wait time.Duration) (err error)

Stop the server using Shutdown method. The wait is set default and minimum to five seconds.

type ServerOptions added in v0.6.0

type ServerOptions struct {
	// Memfs contains the content of file systems to be served in memory.
	// The MemFS instance to be served should be already embedded in Go
	// file, generated using memfs.MemFS.GoEmbed().
	// Otherwise, it will try to read from file system directly.
	//
	// See https://pkg.go.dev/github.com/shuLhan/share/lib/memfs#hdr-Go_embed
	Memfs *memfs.MemFS

	// HandleFS inspect each GET request to Memfs.
	// Some usage of this handler is to check for authorization on
	// specific path, handling redirect, and so on.
	// If nil it means all request are allowed.
	// See FSHandler for more information.
	HandleFS FSHandler

	// Address define listen address, using ip:port format.
	// This field is optional, default to ":80".
	Address string

	// Conn contains custom HTTP server connection.
	// This fields is optional.
	Conn *http.Server

	// ErrorWriter define the writer where output from panic in handler
	// will be written.  Basically this will create new log.Logger and set
	// the default Server.ErrorLog.
	// This field is optional, but if its set it will be used only if Conn
	// is not set by caller.
	ErrorWriter io.Writer

	// The options for Cross-Origin Resource Sharing.
	CORS CORSOptions

	// If true, server generate index.html automatically if its not
	// exist in the directory.
	// The index.html contains the list of files inside the requested
	// path.
	EnableIndexHtml bool
}

ServerOptions define an options to initialize HTTP server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL