Documentation ¶
Overview ¶
Package http implement custom HTTP server with memory file system and simplified routing handler.
Features ¶
The following enhancements are added to Server and Client,
- Simplify registering routing with key binding in Server
- Add support for handling CORS in Server
- Serving files using memfs.MemFS in Server
- Simplify sending body with "application/x-www-form-urlencoded", "multipart/form-data", "application/json" with POST or PUT methods in Client.
- Add support for HTTP Range in Server and Client
- Add support for Server-Sent Events (SSE) in Server. For client see the sub package [sseclient].
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 http.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{ Memfs: &memfs.MemFS{ Opts: &memfs.Options{ Root: `./www`, TryDirect: true, }, }, 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 "git.sr.ht/~shulhan/pakakeh.go/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 http.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.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 http.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()>}
Range request ¶
The standard http package provide http.ServeContent function that support serving resources with Range request, except that it sometime it has an issue.
When server receive,
GET /big Range: bytes=0-
and the requested resources is quite larger, where writing all content of file result in i/o timeout, it is best practice if the server write only partial content and let the client continue with the subsequent Range request.
In the above case, the server should response with,
HTTP/1.1 206 Partial content Content-Range: bytes 0-<limit>/<size> Content-Length: <limit>
Where limit is maximum packet that is reasonable for most of the client. In this server we choose 8MB as limit, see DefRangeLimit.
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. }
Bugs and Limitations ¶
The server does not handle CONNECT method
Missing test for request with content-type multipart-form
Server 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 ¶
- Constants
- Variables
- func CreateMultipartFileHeader(filename string, content []byte) (fh *multipart.FileHeader, err error)
- func DefaultErrorHandler(epr *EndpointRequest)
- func GenerateFormData(mpform *multipart.Form) (contentType, body string, err error)
- func HandleRange(res http.ResponseWriter, req *http.Request, bodyReader io.ReadSeeker, ...)
- func IPAddressOfRequest(headers http.Header, defAddr string) (addr string)
- func MarshalForm(in any) (out url.Values, err error)
- func ParseResponseHeader(raw []byte) (resp *http.Response, rest []byte, err error)
- func ParseXForwardedFor(val string) (clientAddr string, proxyAddrs []string)
- func UnmarshalForm(in url.Values, out interface{}) (err error)
- type CORSOptions
- type Callback
- type CallbackErrorHandler
- type Client
- func (client *Client) Delete(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) Do(req *http.Request) (res *ClientResponse, err error)
- func (client *Client) Download(req DownloadRequest) (res *http.Response, err error)
- func (client *Client) GenerateHTTPRequest(req ClientRequest) (httpReq *http.Request, err error)
- func (client *Client) Get(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) Head(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) Post(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) PostForm(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) PostFormData(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) PostJSON(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) Put(req ClientRequest) (*ClientResponse, error)
- func (client *Client) PutForm(req ClientRequest) (*ClientResponse, error)
- func (client *Client) PutFormData(req ClientRequest) (res *ClientResponse, err error)
- func (client *Client) PutJSON(req ClientRequest) (res *ClientResponse, err error)
- type ClientOptions
- type ClientRequest
- type ClientResponse
- type DownloadRequest
- type Endpoint
- type EndpointRequest
- type EndpointResponse
- type Evaluator
- type FSHandler
- type Range
- type RangePosition
- type RequestMethod
- type RequestType
- type ResponseType
- type SSECallback
- type SSEConn
- type SSEEndpoint
- type Server
- func (srv *Server) HandleFS(res http.ResponseWriter, req *http.Request)
- func (srv *Server) RedirectTemp(res http.ResponseWriter, redirectURL string)
- func (srv *Server) RegisterEndpoint(ep Endpoint) (err error)
- func (srv *Server) RegisterEvaluator(eval Evaluator)
- func (srv *Server) RegisterHandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
- func (srv *Server) RegisterSSE(ep SSEEndpoint) (err error)
- func (srv *Server) ServeHTTP(res http.ResponseWriter, req *http.Request)
- func (srv *Server) Start() (err error)
- func (srv *Server) Stop(wait time.Duration) (err error)
- type ServerOptions
Examples ¶
- Endpoint (ErrorHandler)
- EndpointResponse
- GenerateFormData
- IPAddressOfRequest
- MarshalForm
- ParseContentRange
- ParseMultipartRange
- ParseRange
- ParseXForwardedFor
- Range.Add
- Range.Positions
- Range.String
- Server (CustomHTTPStatusCode)
- Server.RegisterHandleFunc
- UnmarshalForm
- UnmarshalForm (Error)
- UnmarshalForm (Slice)
- UnmarshalForm (Zero)
Constants ¶
const ( AcceptRangesBytes = `bytes` AcceptRangesNone = `none` )
List of header value for HTTP header Accept-Ranges.
const ( ContentEncodingBzip2 = `bzip2` ContentEncodingCompress = `compress` // Using LZW. ContentEncodingGzip = `gzip` ContentEncodingDeflate = `deflate` // Using zlib. )
List of known "Content-Encoding" header values.
const ( ContentTypeBinary = `application/octet-stream` ContentTypeEventStream = `text/event-stream` ContentTypeForm = `application/x-www-form-urlencoded` ContentTypeMultipartByteRanges = `multipart/byteranges` 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` )
List of known "Content-Type" header values.
const ( 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` HeaderAccept = `Accept` HeaderAcceptEncoding = `Accept-Encoding` HeaderAcceptRanges = `Accept-Ranges` HeaderAllow = `Allow` HeaderAuthKeyBearer = `Bearer` HeaderAuthorization = `Authorization` HeaderCacheControl = `Cache-Control` HeaderContentEncoding = `Content-Encoding` HeaderContentLength = `Content-Length` HeaderContentRange = `Content-Range` HeaderContentType = `Content-Type` HeaderCookie = `Cookie` HeaderDate = `Date` HeaderETag = `Etag` HeaderHost = `Host` HeaderIfModifiedSince = `If-Modified-Since` HeaderIfNoneMatch = `If-None-Match` HeaderLastEventID = `Last-Event-ID` HeaderLocation = `Location` HeaderOrigin = `Origin` HeaderRange = `Range` HeaderSetCookie = `Set-Cookie` HeaderUserAgent = `User-Agent` HeaderXForwardedFor = `X-Forwarded-For` // https://en.wikipedia.org/wiki/X-Forwarded-For HeaderXRealIP = `X-Real-Ip` )
List of known header names.
const DefRangeLimit = 8388608
DefRangeLimit limit of content served by server when Range request without end, in example "0-".
Variables ¶
var ( // ErrClientDownloadNoOutput define an error when Client's // DownloadRequest does not define the Output. 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`) )
Functions ¶
func CreateMultipartFileHeader ¶ added in v0.55.0
func CreateMultipartFileHeader(filename string, content []byte) (fh *multipart.FileHeader, err error)
CreateMultipartFileHeader create multipart.FileHeader from raw content with optional filename.
func DefaultErrorHandler ¶
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 *liberrors.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 GenerateFormData ¶
GenerateFormData generate "multipart/form-data" body from mpform.
Example ¶
package main import ( "crypto/rand" "fmt" "log" "mime/multipart" "strings" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" "git.sr.ht/~shulhan/pakakeh.go/lib/test/mock" ) func main() { // Mock the random reader for predictable output. // NOTE: do not do this on real code. rand.Reader = mock.NewRandReader([]byte(`randomseed`)) var data = &multipart.Form{ Value: map[string][]string{ `name`: []string{`test.txt`}, `size`: []string{`42`}, }, } var ( contentType string body string err error ) contentType, body, err = libhttp.GenerateFormData(data) if err != nil { log.Fatal(err) } fmt.Println(`contentType:`, contentType) fmt.Println(`body:`) fmt.Println(strings.ReplaceAll(body, "\r\n", "\n")) }
Output: contentType: multipart/form-data; boundary=72616e646f6d7365656472616e646f6d7365656472616e646f6d73656564 body: --72616e646f6d7365656472616e646f6d7365656472616e646f6d73656564 Content-Disposition: form-data; name="name" test.txt --72616e646f6d7365656472616e646f6d7365656472616e646f6d73656564 Content-Disposition: form-data; name="size" 42 --72616e646f6d7365656472616e646f6d7365656472616e646f6d73656564--
func HandleRange ¶
func HandleRange(res http.ResponseWriter, req *http.Request, bodyReader io.ReadSeeker, contentType string)
HandleRange handle HTTP Range request using "bytes" unit.
The body parameter contains the content of resource being requested that implement Reader and Seeker.
If the http.Request.Method is not GET, or no "Range" in http.Request.Header, it will return all the body RFC7233 S-3.1.
The contentType is optional, if its empty, it will detected by http.ResponseWriter during Write.
It will return HTTP Code,
- 406 http.StatusNotAcceptable, if the Range unit is not "bytes".
- 416 http.StatusRequestedRangeNotSatisfiable, if the request Range start position is greater than resource size.
func IPAddressOfRequest ¶
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 MarshalForm ¶
MarshalForm marshal struct fields tagged with `form:` into url.Values.
The rules for marshaling follow the same rules as in UnmarshalForm.
It will return an error if the input is not pointer to or a struct.
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 = T{ Rat: big.NewRat(`1.2345`), String: `a_string`, Bytes: []byte(`bytes`), Int: 1, F64: 6.4, F32: 3.2, Uint8: 2, Bool: true, } out url.Values err error ) out, err = MarshalForm(in) if err != nil { fmt.Println(err) } fmt.Println(out.Encode())
Output: Bool=true&Int=1&big.Rat=1.2345&bytes=bytes&f32=3.2&f64=6.4¬set=0&string=a_string&uint8=2
func ParseResponseHeader ¶
ParseResponseHeader parse HTTP response header and return it as standard HTTP Response with unreaded packet.
func ParseXForwardedFor ¶
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 ¶
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 manner.
Only the following types are supported: bool, int/intX, uint/uintX, floatX, string, []byte, or type that implement encoding.BinaryUnmarshaler (UnmarshalBinary), json.Unmarshaler (UnmarshalJSON), or encoding.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:1.2345 String:a_string Bytes:[98 121 116 101 115] Int:1 F64:6.4 F32:3.2 NotSet:0 Uint8:2 Bool:true} &{Rat:1.2345 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:1.2345 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 String: Bytes:[] Int:0 F64:0 F32:0 NotSet:0 Uint8:0 Bool:false}
Types ¶
type CORSOptions ¶
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 *liberrors.E, with liberrors.E.Code define the HTTP status code. If error is not nil and not *liberrors.E, server will response with http.StatusInternalServerError status code.
type CallbackErrorHandler ¶
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 ¶
Client is a wrapper for standard http.Client with simplified usabilities, including setting default headers, uncompressing response body.
func NewClient ¶
func NewClient(opts ClientOptions) (client *Client)
NewClient create and initialize new Client.
The client will have net.Dialer.KeepAlive set to 30 seconds, with one http.Transport.MaxIdleConns, and 90 seconds http.Transport.IdleConnTimeout.
func (*Client) Delete ¶
func (client *Client) Delete(req ClientRequest) (res *ClientResponse, err error)
Delete send the DELETE request to server using rpath as target endpoint and params as query parameters. On success, it will return the uncompressed response body.
func (*Client) Do ¶
func (client *Client) Do(req *http.Request) (res *ClientResponse, 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 ¶
func (client *Client) Download(req DownloadRequest) (res *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 ¶
func (client *Client) GenerateHTTPRequest(req ClientRequest) (httpReq *http.Request, err error)
GenerateHTTPRequest generate http.Request from ClientRequest.
For HTTP method GET, CONNECT, DELETE, HEAD, OPTIONS, or TRACE; the [ClientRequest.Params] value should be nil or url.Values. If its not nil, it will be converted as rules below.
For HTTP method PATCH, POST, or PUT; the [ClientRequest.Params] will converted based on [ClientRequest.Type] rules below,
- If Type is RequestTypeQuery and Params is url.Values it will be send as query parameters in the Path.
- If Type is RequestTypeForm and Params is url.Values it will be send as URL encoded in the body.
- If Type is RequestTypeMultipartForm and Params type is *multipart.Form, then it will send as multipart form in the body.
- If Type is RequestTypeJSON and Params is not nil, the Params will be encoded as JSON in the body.
func (*Client) Get ¶
func (client *Client) Get(req ClientRequest) (res *ClientResponse, err error)
Get send the GET request to server using [ClientRequest.Path] as target endpoint and [ClientRequest.Params] as query parameters. On success, it will return the uncompressed response body.
func (*Client) Head ¶
func (client *Client) Head(req ClientRequest) (res *ClientResponse, err error)
Head send the HEAD request to rpath endpoint, with optional hdr and params in query parameters. The returned resBody shoule be always nil.
func (*Client) Post ¶
func (client *Client) Post(req ClientRequest) (res *ClientResponse, err error)
Post send the POST request to rpath without setting "Content-Type". If the params is not nil, it will send as query parameters in the rpath.
func (*Client) PostForm ¶
func (client *Client) PostForm(req ClientRequest) (res *ClientResponse, err error)
PostForm send the POST request to rpath using "application/x-www-form-urlencoded".
func (*Client) PostFormData ¶
func (client *Client) PostFormData(req ClientRequest) (res *ClientResponse, err error)
PostFormData send the POST request to Path with all parameters is send using "multipart/form-data".
func (*Client) PostJSON ¶
func (client *Client) PostJSON(req ClientRequest) (res *ClientResponse, err error)
PostJSON send the POST request with content type set to "application/json" and Params encoded automatically to JSON. The Params must be a type than can be marshalled with json.Marshal or type that implement json.Marshaler.
func (*Client) Put ¶
func (client *Client) Put(req ClientRequest) (*ClientResponse, error)
Put send the HTTP PUT request to rpath with optional, raw body. The Content-Type can be set in the hdr.
func (*Client) PutForm ¶
func (client *Client) PutForm(req ClientRequest) (*ClientResponse, error)
PutForm send the PUT request with params set in body using content type "application/x-www-form-urlencoded".
func (*Client) PutFormData ¶
func (client *Client) PutFormData(req ClientRequest) (res *ClientResponse, err error)
PutFormData send the PUT request with params set in body using content type "multipart/form-data".
func (*Client) PutJSON ¶
func (client *Client) PutJSON(req ClientRequest) (res *ClientResponse, err error)
PutJSON send the PUT request with content type set to "application/json" and params encoded automatically to JSON.
type ClientOptions ¶
type ClientOptions struct { // Headers define default headers that will be send in any request to // server. // This field is optional. Headers http.Header // ServerURL define the server address without path, for example // "https://example.com" or "http://10.148.0.12:8080". // This value should not changed during call of client's method. // 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 ¶
type ClientRequest struct { // Header additional header to be send on request. // This field is optional. Header http.Header // Params define parameter to be send on request. // This field is optional. // It 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 any // 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 // contains filtered or unexported fields }
ClientRequest define the parameters for each Client methods.
type ClientResponse ¶
ClientResponse contains the response from HTTP client request.
type DownloadRequest ¶
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.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 // Method contains HTTP method, default to GET. Method RequestMethod // 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 }
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) ¶
var ( serverOpts = ServerOptions{ Address: `127.0.0.1:8123`, } server *Server ) server, _ = NewServer(serverOpts) var endpointError = Endpoint{ Method: RequestMethodGet, Path: "/", RequestType: RequestTypeQuery, ResponseType: ResponseTypePlain, Call: func(epr *EndpointRequest) ([]byte, error) { return nil, errors.New(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) var ( clientOpts = ClientOptions{ ServerURL: `http://` + serverOpts.Address, } client = NewClient(clientOpts) ) var params = url.Values{} params.Set("error", "400:error with status code") var req = ClientRequest{ Path: `/`, Params: params, } var ( res *ClientResponse err error ) res, err = client.Get(req) if err != nil { log.Println(err) return } fmt.Printf("%d: %s\n", res.HTTPResponse.StatusCode, res.Body) params.Set("error", "error without status code") res, err = client.Get(req) if err != nil { log.Println(err) return } fmt.Printf("%d: %s\n", res.HTTPResponse.StatusCode, res.Body)
Output: 400: error with status code 500: error without status code
type EndpointRequest ¶
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 ¶
type EndpointResponse struct { Data interface{} `json:"data,omitempty"` liberrors.E // 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 liberrors.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 } var ( server *Server err error ) 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.E.Code = http.StatusBadRequest res.E.Message = "empty parameter id" return nil, res } if id == "0" { // If the EndpointResponse.Code is 0, it will // default to http.StatusInternalServerError res.E.Message = "id value 0 cause internal server error" return nil, res } res.E.Code = http.StatusOK res.Data = &myData{ ID: id, } return json.Marshal(res) }, }) if err != nil { log.Fatal(err) } go func() { var errStart = server.Start() if errStart != nil { log.Fatal(errStart) } }() time.Sleep(1 * time.Second) var ( clientOpts = ClientOptions{ ServerURL: `http://127.0.0.1:7016`, } cl = NewClient(clientOpts) params = url.Values{} req = ClientRequest{ Path: `/`, Params: params, } res *ClientResponse ) // Test call endpoint without "id" parameter. res, err = cl.Get(req) if err != nil { log.Fatal(err) } fmt.Printf("GET / => %s\n", res.Body) // Test call endpoint with "id" parameter set to "0", it should return // HTTP status 500 with custom message. params.Set("id", "0") req.Params = params res, err = cl.Get(req) if err != nil { log.Fatal(err) } fmt.Printf("GET /?id=0 => %s\n", res.Body) // Test with "id" parameter is set. params.Set("id", "1000") req.Params = params res, err = cl.Get(req) if err != nil { log.Fatal(err) } fmt.Printf("GET /?id=1000 => %s\n", res.Body)
Output: GET / => {"message":"empty parameter id","code":400} GET /?id=0 => {"message":"id value 0 cause internal server error","code":500} GET /?id=1000 => {"data":{"ID":"1000"},"code":200}
func (*EndpointResponse) Error ¶
func (epr *EndpointResponse) Error() string
func (*EndpointResponse) Unwrap ¶
func (epr *EndpointResponse) Unwrap() (err error)
Unwrap return the error as instance of *liberrors.E.
type Evaluator ¶
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 ¶
FSHandler define the function to inspect each GET request to Server memfs.MemFS instance. The node parameter contains the requested file inside the memfs or nil if the file does not exist.
If the handler return non-nil *memfs.Node, server will continue processing the node, writing the memfs.Node content type, body, and so on.
If the handler return nil, 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 Range ¶
type Range struct {
// contains filtered or unexported fields
}
Range define the unit and list of start-end positions for resource.
func NewRange ¶
NewRange create new Range with specified unit. The default unit is "bytes" if its empty.
func ParseMultipartRange ¶
ParseMultipartRange parse "multipart/byteranges" response body. Each Content-Range position and body part in the multipart will be stored under RangePosition.
Example ¶
package main import ( "bytes" "fmt" "log" "strings" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) func main() { var ( boundary = `zxcv` ) var body = `--zxcv Content-Range: bytes 0-6/50 Part 1 --zxcv Missing Content-Range header, skipped. --zxcv Content-Range: bytes 7-13 Invalid Content-Range, missing size, skipped. --zxcv Content-Range: bytes 14-19/50 Part 2 --zxcv-- ` body = strings.ReplaceAll(body, "\n", "\r\n") var ( reader = bytes.NewReader([]byte(body)) r *libhttp.Range err error ) r, err = libhttp.ParseMultipartRange(reader, boundary) if err != nil { log.Fatal(err) } var pos *libhttp.RangePosition for _, pos = range r.Positions() { fmt.Printf("%s: %s\n", pos.String(), pos.Content()) } }
Output: 0-6: Part 1 14-19: Part 2
func ParseRange ¶
ParseRange parses raw range value in the following format,
range = unit "=" position *("," position) unit = 1*VCHAR position = "-" last / start "-" / start "-" end last = 1*DIGIT start = 1*DIGIT end = 1*DIGIT
An invalid position will be skipped.
Example ¶
package main import ( "fmt" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) func main() { var r libhttp.Range // Empty range due to missing "=". r = libhttp.ParseRange(`bytes`) fmt.Println(r.String()) r = libhttp.ParseRange(`bytes=10-`) fmt.Println(r.String()) // The "20-30" is overlap with "10-". r = libhttp.ParseRange(`bytes=10-,20-30`) fmt.Println(r.String()) // The "10-" is ignored since its overlap with the first range // "20-30". r = libhttp.ParseRange(`bytes=20 - 30 , 10 -`) fmt.Println(r.String()) r = libhttp.ParseRange(`bytes=10-20`) fmt.Println(r.String()) r = libhttp.ParseRange(`bytes=-20`) fmt.Println(r.String()) r = libhttp.ParseRange(`bytes=0-9,10-19,-20`) fmt.Println(r.String()) r = libhttp.ParseRange(`bytes=0-`) fmt.Println(r.String()) // The only valid position here is 0-9, 10-19, and -20. // The x, -x, x-9, 0-x, 0-9-, and -0-9 is not valid position. // The -10 is overlap with -20. r = libhttp.ParseRange(`bytes=,x,-x,x-9,0-x,0-9-,-0-9,0-9,10-19,-20,-10,`) fmt.Println(r.String()) }
Output: bytes=10- bytes=10- bytes=20-30 bytes=10-20 bytes=-20 bytes=0-9,10-19,-20 bytes=0- bytes=0-9,10-19,-20
func (*Range) Add ¶
Add start and end as requested position to Range. The start and end position is inclusive, closed interval [start, end], with end position must equal or greater than start position, unless its zero. For example,
- [0,+x] is valid, from offset 0 until x+1.
- [0,0] is valid and equal to first byte (but unusual).
- [+x,+y] is valid iff x <= y.
- [+x,-y] is invalid.
- [-x,+y] is invalid.
The start or end can be nil, but not both. For example,
- [nil,+x] is valid, equal to "-x" or the last x bytes.
- [nil,0] is invalid.
- [nil,-x] is invalid.
- [x,nil] is valid, equal to "x-" or from offset x until end of file.
- [-x,nil] is invalid.
The new position will be added and return true iff it does not overlap with existing list.
Example ¶
package main import ( "fmt" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) func ptrInt64(v int64) *int64 { return &v } func main() { var listpos = []struct { start *int64 end *int64 }{ {ptrInt64(0), ptrInt64(9)}, // OK. {ptrInt64(0), ptrInt64(5)}, // Overlap with [0,9]. {ptrInt64(9), ptrInt64(19)}, // Overlap with [0,9]. {ptrInt64(10), ptrInt64(19)}, // OK. {ptrInt64(19), ptrInt64(20)}, // Overlap with [10,19]. {ptrInt64(20), ptrInt64(19)}, // End less than start. {nil, ptrInt64(10)}, // OK. {nil, ptrInt64(20)}, // Overlap with [nil,10]. {ptrInt64(20), nil}, // Overlap with [nil,10]. {ptrInt64(30), ptrInt64(40)}, // Overlap with [20,nil]. {ptrInt64(30), nil}, // Overlap with [20,nil]. } var r = libhttp.NewRange(``) for _, pos := range listpos { fmt.Println(r.Add(pos.start, pos.end), r.String()) } }
Output: true bytes=0-9 false bytes=0-9 false bytes=0-9 true bytes=0-9,10-19 false bytes=0-9,10-19 false bytes=0-9,10-19 true bytes=0-9,10-19,-10 false bytes=0-9,10-19,-10 false bytes=0-9,10-19,-10 true bytes=0-9,10-19,-10,30-40 false bytes=0-9,10-19,-10,30-40
func (*Range) Positions ¶
func (r *Range) Positions() []*RangePosition
Positions return the list of range position.
Example ¶
package main import ( "fmt" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) func ptrInt64(v int64) *int64 { return &v } func main() { var r = libhttp.NewRange(``) fmt.Println(r.Positions()) // Empty positions. r.Add(ptrInt64(10), ptrInt64(20)) fmt.Println(r.Positions()) }
Output: [] [10-20]
func (*Range) String ¶
String return the Range as value for HTTP header. It will return an empty string if no position registered.
Example ¶
package main import ( "fmt" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) func ptrInt64(v int64) *int64 { return &v } func main() { var r = libhttp.NewRange(`MyUnit`) fmt.Println(r.String()) // Empty range will return empty string. r.Add(ptrInt64(0), ptrInt64(9)) fmt.Println(r.String()) }
Output: myunit=0-9
type RangePosition ¶
type RangePosition struct {
// contains filtered or unexported fields
}
RangePosition contains the parsed value of Content-Range header.
func ParseContentRange ¶
func ParseContentRange(v string) (pos *RangePosition)
ParseContentRange parse the HTTP header "Content-Range" value, as response from server, with the following format,
Content-Range = unit SP valid-range / invalid-range SP = " " valid-range = position "/" size invalid-range = "*" "/" size position = start "-" end size = 1*DIGIT / "*" start = 1*DIGIT end = 1*DIGIT
It will return nil if the v is invalid.
Example ¶
package main import ( "fmt" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) func main() { fmt.Println(libhttp.ParseContentRange(`bytes 10-/20`)) // Invalid, missing end. fmt.Println(libhttp.ParseContentRange(`bytes 10-19/20`)) // OK fmt.Println(libhttp.ParseContentRange(`bytes -10/20`)) // Invalid, missing start. fmt.Println(libhttp.ParseContentRange(`10-20/20`)) // Invalid, missing unit. fmt.Println(libhttp.ParseContentRange(`bytes 10-`)) // Invalid, missing "/size". fmt.Println(libhttp.ParseContentRange(`bytes -10/x`)) // Invalid, invalid "size". fmt.Println(libhttp.ParseContentRange(`bytes`)) // Invalid, missing position. }
Output: <nil> 10-19 <nil> <nil> <nil> <nil> <nil>
func (RangePosition) Content ¶
func (pos RangePosition) Content() []byte
Content return the range content body in multipart.
func (RangePosition) ContentRange ¶
func (pos RangePosition) ContentRange(unit string, size int64) (v string)
ContentRange return the string that can be used for HTTP Content-Range header value.
func (RangePosition) String ¶
func (pos RangePosition) String() string
type RequestMethod ¶
type RequestMethod string
RequestMethod define type of HTTP method.
const ( RequestMethodConnect RequestMethod = http.MethodConnect RequestMethodDelete RequestMethod = http.MethodDelete RequestMethodGet RequestMethod = http.MethodGet RequestMethodHead RequestMethod = http.MethodHead RequestMethodOptions RequestMethod = http.MethodOptions RequestMethodPatch RequestMethod = http.MethodPatch RequestMethodPost RequestMethod = http.MethodPost RequestMethodPut RequestMethod = http.MethodPut RequestMethodTrace RequestMethod = http.MethodTrace )
List of known HTTP methods.
type RequestType ¶
type RequestType string
RequestType define type of HTTP request.
const ( RequestTypeNone RequestType = `` RequestTypeQuery RequestType = `query` RequestTypeForm RequestType = `form-urlencoded` RequestTypeHTML RequestType = `html` RequestTypeMultipartForm RequestType = `form-data` RequestTypeJSON RequestType = `json` RequestTypeXML RequestType = `xml` )
List of valid request type.
func (RequestType) String ¶
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 string
ResponseType define the content type for HTTP response.
const ( // ResponseTypeNone skip writing header Content-Type and status // code, it will handled manually by [Endpoint.Call]. ResponseTypeNone ResponseType = `` ResponseTypeBinary ResponseType = `binary` ResponseTypeHTML ResponseType = `html` ResponseTypeJSON ResponseType = `json` ResponseTypePlain ResponseType = `plain` ResponseTypeXML ResponseType = `xml` )
List of valid response type.
func (ResponseType) String ¶
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 SSECallback ¶
type SSECallback func(sse *SSEConn)
SSECallback define the handler for Server-Sent Events (SSE).
SSECallback type pass SSEConn that contains original HTTP request. This allow the server to check for header "Last-Event-ID" and/or for authentication. Remember that "the original http.Request.Body must not be used" according to http.Hijacker documentation.
type SSEConn ¶
SSEConn define the connection when the SSE request accepted by server.
func (*SSEConn) WriteEvent ¶
WriteEvent write message with optional event type and id to client.
The "event" parameter is optional. If its empty, no "event:" line will be send to client.
The "data" parameter must not be empty, otherwise no message will be send. If "data" value contains new line character ('\n'), the message will be split into multiple "data:".
The id parameter is optional. If its nil, it will be ignored. if its non-nil and empty, it will be send as empty ID.
It will return an error if its failed to write to peer connection.
type SSEEndpoint ¶
type SSEEndpoint struct { // Call handler that will called when request to Path accepted. Call SSECallback // Path where server accept the request for SSE. Path string // KeepAliveInterval define the interval where server will send an // empty message to active connection periodically. // This field is optional, default and minimum value is 5 seconds. KeepAliveInterval time.Duration }
SSEEndpoint endpoint to create Server-Sent Events (SSE) on server.
For creating the SSE client see subpackage [sseclient].
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"` } var ( exp = CustomResponse{ Status: http.StatusBadRequest, } opts = ServerOptions{ Address: "127.0.0.1:8123", } srv *Server err error ) srv, err = NewServer(opts) if err != nil { log.Fatal(err) } go func() { err = srv.Start() if err != nil { log.Println(err) } }() defer func() { _ = srv.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 = srv.registerPost(epCustom) if err != nil { log.Println(err) return } // Wait for the server fully started. time.Sleep(1 * time.Second) var ( clientOpts = ClientOptions{ ServerURL: `http://127.0.0.1:8123`, } client = NewClient(clientOpts) req = ClientRequest{ Path: epCustom.Path, } res *ClientResponse ) res, err = client.PostJSON(req) if err != nil { log.Println(err) return } fmt.Printf("%d\n", res.HTTPResponse.StatusCode) fmt.Printf("%s\n", res.Body)
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 ¶
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 [ServerOptions.Memfs] is not nil.
If the request Path exists and [ServerOptions.HandleFS] 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 ¶
RegisterEndpoint register the Endpoint based on Method. If [Endpoint.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 or OPTIONS does not have any effect because it already handled automatically by server.
Endpoint with Method CONNECT or TRACE will return an error because its not supported, yet.
func (*Server) RegisterEvaluator ¶
RegisterEvaluator register HTTP middleware that will be called before [Endpoint.Eval] and [Endpoint.Call] is called.
func (*Server) RegisterHandleFunc ¶ added in v0.58.0
func (srv *Server) RegisterHandleFunc( pattern string, handler func(http.ResponseWriter, *http.Request), )
RegisterHandlerFunc register a pattern with a handler, similar to http.ServeMux.HandleFunc. The pattern follow the Go 1.22 format:
[METHOD] PATH
The METHOD is optional, default to GET. The PATH must not contains the domain name and space. Unlike standard library, variable in PATH is read using ":var" not "{var}". This endpoint will accept any content type and return the body as is; it is up to the handler to read and set the content type and the response headers.
If the METHOD and/or PATH is already registered it will panic.
Example ¶
var serverOpts = ServerOptions{} server, _ := NewServer(serverOpts) server.RegisterHandleFunc(`PUT /api/book/:id`, func(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Fprintf(w, "Request.URL: %s\n", r.URL) fmt.Fprintf(w, "Request.Form: %+v\n", r.Form) fmt.Fprintf(w, "Request.PostForm: %+v\n", r.PostForm) }, ) var respRec = httptest.NewRecorder() var body = []byte(`title=BahasaPemrogramanGo&author=Shulhan`) var req = httptest.NewRequest(`PUT`, `/api/book/123`, bytes.NewReader(body)) req.Header.Set(`Content-Type`, `application/x-www-form-urlencoded`) server.ServeHTTP(respRec, req) var resp = respRec.Result() body, _ = io.ReadAll(resp.Body) fmt.Println(resp.Status) fmt.Printf("%s", body)
Output: 200 OK Request.URL: /api/book/123 Request.Form: map[id:[123]] Request.PostForm: map[author:[Shulhan] title:[BahasaPemrogramanGo]]
func (*Server) RegisterSSE ¶
func (srv *Server) RegisterSSE(ep SSEEndpoint) (err error)
RegisterSSE register Server-Sent Events endpoint. It will return an error if the [SSEEndpoint.Call] field is not set or ErrEndpointAmbiguous if the same path is already registered.
type ServerOptions ¶
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/git.sr.ht/~shulhan/pakakeh.go/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.
Source Files ¶
- callback.go
- callback_error_handler.go
- client.go
- client_options.go
- client_request.go
- client_response.go
- cors_options.go
- download_request.go
- endpoint.go
- endpoint_request.go
- endpoint_response.go
- evaluator.go
- form.go
- fs_handler.go
- http.go
- multipart_form.go
- range.go
- range_position.go
- request_method.go
- request_type.go
- response.go
- response_type.go
- route.go
- server.go
- server_options.go
- sse_conn.go
- sse_endpoint.go