implemented HTTP 1.0/1.1 server
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
sh "serveg/internal/string_helper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const maxHeaderBytes = 16 * 1024
|
||||
const initialWriteBufCap = 512
|
||||
|
||||
type mapStrStr map[string]string
|
||||
type HeadersMap map[string]string
|
||||
|
||||
type Connection struct {
|
||||
socket *net.TCPConn
|
||||
r *bufio.Reader
|
||||
wbuf sh.StringBuilder
|
||||
}
|
||||
|
||||
func NewConnection(socket *net.TCPConn) *Connection {
|
||||
return &Connection{
|
||||
socket: socket,
|
||||
r: bufio.NewReaderSize(socket, maxHeaderBytes),
|
||||
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
|
||||
}
|
||||
}
|
||||
|
||||
func HeaderValueContains(sourceSplit []string, target string) bool {
|
||||
for _, v := range sourceSplit {
|
||||
v = strings.TrimSpace(v)
|
||||
if strings.EqualFold(v, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
sh "serveg/internal/string_helper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Method string
|
||||
Protocol string
|
||||
// url-encoded characters (%D0), query params (?v=123)
|
||||
PathRaw string
|
||||
Path string
|
||||
QueryParams mapStrStr
|
||||
// keys are in lower case
|
||||
// duplicate keys are joined with ", "
|
||||
Headers HeadersMap
|
||||
// if true, don't close connection after response
|
||||
WantsKeepAlive bool
|
||||
}
|
||||
|
||||
// Read request (body not supported)
|
||||
// Returns valid request pointer. If err != nil, req is partially parsed.
|
||||
// It doesn't enforce "Host" header because i don't need it in this project.
|
||||
//
|
||||
// if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
|
||||
// // Request is malformed, socket buffer contains unread garbage data.
|
||||
// // Send error code and close connection.
|
||||
// } else {
|
||||
// // something bad happened with the connection, just close it
|
||||
// }
|
||||
func (conn *Connection) ReadRequest() (req *Request, err error) {
|
||||
req = new(Request)
|
||||
var readN int
|
||||
// METHOD /path HTTP/ver
|
||||
if err = conn.readFirstLine(req, &readN); err != nil {
|
||||
return
|
||||
}
|
||||
// key: value
|
||||
// ...
|
||||
// empty line
|
||||
if err = conn.readHeaders(req, &readN); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// This server doesn't accept request bodies.
|
||||
// Reject connection rather than desync the stream.
|
||||
contentLength := req.Headers["content-length"]
|
||||
transferEncoding := req.Headers["transfer-encoding"]
|
||||
if (contentLength != "" && contentLength != "0") ||
|
||||
transferEncoding != "" {
|
||||
err = NewHttpError(StatusBadRequest,
|
||||
"request bodies are not supported")
|
||||
}
|
||||
|
||||
// set WantsKeepAlive
|
||||
connectionStrSplit := strings.Split(req.Headers["connection"], ",")
|
||||
if req.Protocol == "HTTP/1.0" {
|
||||
req.WantsKeepAlive = HeaderValueContains(connectionStrSplit, "keep-alive")
|
||||
} else {
|
||||
req.WantsKeepAlive = !HeaderValueContains(connectionStrSplit, "close")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *Connection) readLineBelowLimit(readN *int) (string, error) {
|
||||
// read up to maxHeaderBytes
|
||||
slice, notLineEnd, err := conn.r.ReadLine()
|
||||
if err != nil {
|
||||
// something is wrong with tcp socket
|
||||
return "", err
|
||||
}
|
||||
if notLineEnd {
|
||||
// buffer end was reached but no '\n' found
|
||||
return "", NewHttpError(StatusBadRequest, "line end not found")
|
||||
}
|
||||
|
||||
*readN += len(slice) + 1 // \n
|
||||
if *readN > maxHeaderBytes {
|
||||
return "", NewHttpError(StatusRequestHeaderFieldsTooLarge,
|
||||
"headers too large")
|
||||
}
|
||||
|
||||
return strings.TrimRight(string(slice), " \r\n"), nil
|
||||
}
|
||||
|
||||
func (conn *Connection) readFirstLine(req *Request, readN *int) error {
|
||||
line, err := conn.readLineBelowLimit(readN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// skip one empty line
|
||||
// some clients send it between requests
|
||||
if line == "" {
|
||||
line, err = conn.readLineBelowLimit(readN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) != 3 || !strings.HasPrefix(parts[2], "HTTP/") {
|
||||
return NewHttpError(StatusBadRequest,
|
||||
"malformed request line '"+line+"'")
|
||||
}
|
||||
req.Method = parts[0]
|
||||
// url-encoded characters (%D0), query params (?v=123)
|
||||
req.PathRaw = parts[1]
|
||||
req.Protocol = parts[2]
|
||||
|
||||
if req.Protocol != "HTTP/1.0" && req.Protocol != "HTTP/1.1" {
|
||||
return NewHttpError(StatusHTTPVersionNotSupported,
|
||||
"request protocol not supported: "+req.Protocol)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(req.PathRaw)
|
||||
if err != nil {
|
||||
return NewHttpError(StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
req.Path = sh.ResolveSafePath(u.Path)
|
||||
if req.Path == "" {
|
||||
return NewHttpError(StatusForbidden,
|
||||
"unsafe request path '"+u.Path+"'")
|
||||
}
|
||||
req.QueryParams = make(mapStrStr)
|
||||
for key, values := range u.Query() {
|
||||
req.QueryParams[key] = values[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connection) readHeaders(req *Request, readN *int) error {
|
||||
req.Headers = make(HeadersMap)
|
||||
for {
|
||||
line, err := conn.readLineBelowLimit(readN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// empty line means end of headers
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
i := strings.IndexByte(line, ':')
|
||||
if i < 1 {
|
||||
return NewHttpError(StatusBadRequest,
|
||||
"not a header line '"+line+"'")
|
||||
}
|
||||
key := line[:i]
|
||||
firstB, lastB := key[0], key[i-1]
|
||||
if firstB == ' ' || firstB == '\t' ||
|
||||
lastB == ' ' || lastB == '\t' {
|
||||
return NewHttpError(StatusBadRequest,
|
||||
"header name must not start or end with space")
|
||||
}
|
||||
key = strings.ToLower(key)
|
||||
|
||||
// val can be empty
|
||||
val := strings.TrimSpace(line[i+1:])
|
||||
// if header repeats, join values with ", "
|
||||
existing := req.Headers[key]
|
||||
if existing != "" {
|
||||
val = existing + ", " + val
|
||||
}
|
||||
req.Headers[key] = val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
sh "serveg/internal/string_helper"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type responseBodyType uint8
|
||||
|
||||
const (
|
||||
responseBodyEmpty = responseBodyType(iota)
|
||||
responseBodySmall
|
||||
responseBodyBig
|
||||
)
|
||||
|
||||
const TextPlain = "text/plain; charset=utf-8"
|
||||
|
||||
type Response struct {
|
||||
Status StatusCode
|
||||
Headers HeadersMap // keys are in lower case
|
||||
omitBody bool // don't add body and content-* headers for HEAD request
|
||||
bodyType responseBodyType
|
||||
bodySmall []byte // small body is sent in one write() call with headers
|
||||
bodyBig io.WriterTo // big body, sent as multiple write() calls to socket
|
||||
}
|
||||
|
||||
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||
|
||||
func (conn *Connection) CreateResponse(
|
||||
status StatusCode, req *Request, wantsClose bool,
|
||||
) *Response {
|
||||
res := &Response{
|
||||
Status: status,
|
||||
Headers: HeadersMap{
|
||||
"date": time.Now().UTC().Format(TimeFormat),
|
||||
},
|
||||
}
|
||||
|
||||
res.omitBody = req.Method == "HEAD"
|
||||
if !res.omitBody &&
|
||||
status >= 200 &&
|
||||
status != StatusNoContent &&
|
||||
status != StatusNotModified {
|
||||
// content-length 0 is mandatory for most responses
|
||||
res.Headers["content-length"] = "0"
|
||||
}
|
||||
|
||||
if wantsClose || !req.WantsKeepAlive {
|
||||
res.Headers["connection"] = "close"
|
||||
} else if req.Protocol == "HTTP/1.0" {
|
||||
res.Headers["connection"] = "keep-alive"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (res *Response) AttachBodySmall(body []byte, contentType string) {
|
||||
if res.omitBody {
|
||||
return
|
||||
}
|
||||
|
||||
if res.bodyType != responseBodyEmpty {
|
||||
panic("response already has body")
|
||||
}
|
||||
res.bodyType = responseBodySmall
|
||||
res.bodySmall = body
|
||||
lenStr := strconv.FormatUint(uint64(len(body)), 10)
|
||||
res.Headers["content-length"] = lenStr
|
||||
res.Headers["content-type"] = contentType
|
||||
}
|
||||
func (res *Response) AttachBodySmallStr(body string, contentType string) {
|
||||
res.AttachBodySmall(sh.UnsafeGetStrBytes(body), contentType)
|
||||
}
|
||||
|
||||
func (res *Response) AttachBodyBig(body io.WriterTo,
|
||||
size uint64, contentType string) {
|
||||
|
||||
if res.omitBody {
|
||||
return
|
||||
}
|
||||
if res.bodyType != responseBodyEmpty {
|
||||
panic("response already has body")
|
||||
}
|
||||
res.bodyType = responseBodyBig
|
||||
res.bodyBig = body
|
||||
lenStr := strconv.FormatUint(size, 10)
|
||||
res.Headers["content-length"] = lenStr
|
||||
res.Headers["content-type"] = contentType
|
||||
}
|
||||
|
||||
var ErrInvalidHeader = errors.New("http: header key or value contains CR or LF")
|
||||
|
||||
func (conn *Connection) SendResponse(res *Response) error {
|
||||
sb := &conn.wbuf
|
||||
sb.Reset()
|
||||
|
||||
// first line
|
||||
// space before \r\n is required by standard
|
||||
sb.AddS("HTTP/1.1 ").AddU(uint64(res.Status)).AddS(" \r\n")
|
||||
|
||||
// headers
|
||||
for k, v := range res.Headers {
|
||||
// prevent response splitting / header injection
|
||||
if strings.ContainsAny(k, "\r\n:") || strings.ContainsAny(v, "\r\n") {
|
||||
return ErrInvalidHeader
|
||||
}
|
||||
sb.AddS(k).AddS(": ").AddS(v).AddS("\r\n")
|
||||
}
|
||||
// empty line
|
||||
sb.AddS("\r\n")
|
||||
|
||||
switch res.bodyType {
|
||||
case responseBodyEmpty:
|
||||
// net.Conn.Write follows the io.Writer contract: it returns a non-nil
|
||||
// error whenever n < len(p), so no loop is needed.
|
||||
if _, err := conn.socket.Write(conn.wbuf.Buf); err != nil {
|
||||
return err
|
||||
}
|
||||
case responseBodySmall:
|
||||
// write headers and body in one socket.Write() call
|
||||
bbb := net.Buffers{sb.Buf, res.bodySmall}
|
||||
if _, err := bbb.WriteTo(conn.socket); err != nil {
|
||||
return err
|
||||
}
|
||||
case responseBodyBig:
|
||||
// write headers, then write big body
|
||||
if _, err := conn.socket.Write(conn.wbuf.Buf); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := res.bodyBig.WriteTo(conn.socket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connection) SendErrorResponse(
|
||||
httpErr *HttpError, req *Request, wantsClose bool,
|
||||
) error {
|
||||
|
||||
res := conn.CreateResponse(httpErr.Status, req, wantsClose)
|
||||
// attach error message if method allows
|
||||
res.AttachBodySmallStr(httpErr.Error(), TextPlain)
|
||||
return conn.SendResponse(res)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type StatusCode uint16
|
||||
|
||||
func (c StatusCode) String() string {
|
||||
numStr := strconv.FormatUint(uint64(c), 10)
|
||||
return statusText(c) + " (" + numStr + ")"
|
||||
}
|
||||
|
||||
/// Copied from /usr/lib/go/src/net/http/status.go
|
||||
/// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// HTTP status codes as registered with IANA.
|
||||
// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
const (
|
||||
StatusContinue StatusCode = 100 // RFC 9110, 15.2.1
|
||||
StatusSwitchingProtocols StatusCode = 101 // RFC 9110, 15.2.2
|
||||
StatusProcessing StatusCode = 102 // RFC 2518, 10.1
|
||||
StatusEarlyHints StatusCode = 103 // RFC 8297
|
||||
|
||||
StatusOK StatusCode = 200 // RFC 9110, 15.3.1
|
||||
StatusCreated StatusCode = 201 // RFC 9110, 15.3.2
|
||||
StatusAccepted StatusCode = 202 // RFC 9110, 15.3.3
|
||||
StatusNonAuthoritativeInfo StatusCode = 203 // RFC 9110, 15.3.4
|
||||
StatusNoContent StatusCode = 204 // RFC 9110, 15.3.5
|
||||
StatusResetContent StatusCode = 205 // RFC 9110, 15.3.6
|
||||
StatusPartialContent StatusCode = 206 // RFC 9110, 15.3.7
|
||||
StatusMultiStatus StatusCode = 207 // RFC 4918, 11.1
|
||||
StatusAlreadyReported StatusCode = 208 // RFC 5842, 7.1
|
||||
StatusIMUsed StatusCode = 226 // RFC 3229, 10.4.1
|
||||
|
||||
StatusMultipleChoices StatusCode = 300 // RFC 9110, 15.4.1
|
||||
StatusMovedPermanently StatusCode = 301 // RFC 9110, 15.4.2
|
||||
StatusFound StatusCode = 302 // RFC 9110, 15.4.3
|
||||
StatusSeeOther StatusCode = 303 // RFC 9110, 15.4.4
|
||||
StatusNotModified StatusCode = 304 // RFC 9110, 15.4.5
|
||||
StatusUseProxy StatusCode = 305 // RFC 9110, 15.4.6
|
||||
_ StatusCode = 306 // RFC 9110, 15.4.7 (Unused)
|
||||
StatusTemporaryRedirect StatusCode = 307 // RFC 9110, 15.4.8
|
||||
StatusPermanentRedirect StatusCode = 308 // RFC 9110, 15.4.9
|
||||
|
||||
StatusBadRequest StatusCode = 400 // RFC 9110, 15.5.1
|
||||
StatusUnauthorized StatusCode = 401 // RFC 9110, 15.5.2
|
||||
StatusPaymentRequired StatusCode = 402 // RFC 9110, 15.5.3
|
||||
StatusForbidden StatusCode = 403 // RFC 9110, 15.5.4
|
||||
StatusNotFound StatusCode = 404 // RFC 9110, 15.5.5
|
||||
StatusMethodNotAllowed StatusCode = 405 // RFC 9110, 15.5.6
|
||||
StatusNotAcceptable StatusCode = 406 // RFC 9110, 15.5.7
|
||||
StatusProxyAuthRequired StatusCode = 407 // RFC 9110, 15.5.8
|
||||
StatusRequestTimeout StatusCode = 408 // RFC 9110, 15.5.9
|
||||
StatusConflict StatusCode = 409 // RFC 9110, 15.5.10
|
||||
StatusGone StatusCode = 410 // RFC 9110, 15.5.11
|
||||
StatusLengthRequired StatusCode = 411 // RFC 9110, 15.5.12
|
||||
StatusPreconditionFailed StatusCode = 412 // RFC 9110, 15.5.13
|
||||
StatusRequestEntityTooLarge StatusCode = 413 // RFC 9110, 15.5.14
|
||||
StatusRequestURITooLong StatusCode = 414 // RFC 9110, 15.5.15
|
||||
StatusUnsupportedMediaType StatusCode = 415 // RFC 9110, 15.5.16
|
||||
StatusRequestedRangeNotSatisfiable StatusCode = 416 // RFC 9110, 15.5.17
|
||||
StatusExpectationFailed StatusCode = 417 // RFC 9110, 15.5.18
|
||||
StatusTeapot StatusCode = 418 // RFC 9110, 15.5.19 (Unused)
|
||||
StatusMisdirectedRequest StatusCode = 421 // RFC 9110, 15.5.20
|
||||
StatusUnprocessableEntity StatusCode = 422 // RFC 9110, 15.5.21
|
||||
StatusLocked StatusCode = 423 // RFC 4918, 11.3
|
||||
StatusFailedDependency StatusCode = 424 // RFC 4918, 11.4
|
||||
StatusTooEarly StatusCode = 425 // RFC 8470, 5.2.
|
||||
StatusUpgradeRequired StatusCode = 426 // RFC 9110, 15.5.22
|
||||
StatusPreconditionRequired StatusCode = 428 // RFC 6585, 3
|
||||
StatusTooManyRequests StatusCode = 429 // RFC 6585, 4
|
||||
StatusRequestHeaderFieldsTooLarge StatusCode = 431 // RFC 6585, 5
|
||||
StatusUnavailableForLegalReasons StatusCode = 451 // RFC 7725, 3
|
||||
|
||||
StatusInternalServerError StatusCode = 500 // RFC 9110, 15.6.1
|
||||
StatusNotImplemented StatusCode = 501 // RFC 9110, 15.6.2
|
||||
StatusBadGateway StatusCode = 502 // RFC 9110, 15.6.3
|
||||
StatusServiceUnavailable StatusCode = 503 // RFC 9110, 15.6.4
|
||||
StatusGatewayTimeout StatusCode = 504 // RFC 9110, 15.6.5
|
||||
StatusHTTPVersionNotSupported StatusCode = 505 // RFC 9110, 15.6.6
|
||||
StatusVariantAlsoNegotiates StatusCode = 506 // RFC 2295, 8.1
|
||||
StatusInsufficientStorage StatusCode = 507 // RFC 4918, 11.5
|
||||
StatusLoopDetected StatusCode = 508 // RFC 5842, 7.2
|
||||
StatusNotExtended StatusCode = 510 // RFC 2774, 7
|
||||
StatusNetworkAuthenticationRequired StatusCode = 511 // RFC 6585, 6
|
||||
)
|
||||
|
||||
func statusText(c StatusCode) string {
|
||||
switch c {
|
||||
case StatusContinue:
|
||||
return "Continue"
|
||||
case StatusSwitchingProtocols:
|
||||
return "Switching Protocols"
|
||||
case StatusProcessing:
|
||||
return "Processing"
|
||||
case StatusEarlyHints:
|
||||
return "Early Hints"
|
||||
case StatusOK:
|
||||
return "OK"
|
||||
case StatusCreated:
|
||||
return "Created"
|
||||
case StatusAccepted:
|
||||
return "Accepted"
|
||||
case StatusNonAuthoritativeInfo:
|
||||
return "Non-Authoritative Information"
|
||||
case StatusNoContent:
|
||||
return "No Content"
|
||||
case StatusResetContent:
|
||||
return "Reset Content"
|
||||
case StatusPartialContent:
|
||||
return "Partial Content"
|
||||
case StatusMultiStatus:
|
||||
return "Multi-Status"
|
||||
case StatusAlreadyReported:
|
||||
return "Already Reported"
|
||||
case StatusIMUsed:
|
||||
return "IM Used"
|
||||
case StatusMultipleChoices:
|
||||
return "Multiple Choices"
|
||||
case StatusMovedPermanently:
|
||||
return "Moved Permanently"
|
||||
case StatusFound:
|
||||
return "Found"
|
||||
case StatusSeeOther:
|
||||
return "See Other"
|
||||
case StatusNotModified:
|
||||
return "Not Modified"
|
||||
case StatusUseProxy:
|
||||
return "Use Proxy"
|
||||
case StatusTemporaryRedirect:
|
||||
return "Temporary Redirect"
|
||||
case StatusPermanentRedirect:
|
||||
return "Permanent Redirect"
|
||||
case StatusBadRequest:
|
||||
return "Bad Request"
|
||||
case StatusUnauthorized:
|
||||
return "Unauthorized"
|
||||
case StatusPaymentRequired:
|
||||
return "Payment Required"
|
||||
case StatusForbidden:
|
||||
return "Forbidden"
|
||||
case StatusNotFound:
|
||||
return "Not Found"
|
||||
case StatusMethodNotAllowed:
|
||||
return "Method Not Allowed"
|
||||
case StatusNotAcceptable:
|
||||
return "Not Acceptable"
|
||||
case StatusProxyAuthRequired:
|
||||
return "Proxy Authentication Required"
|
||||
case StatusRequestTimeout:
|
||||
return "Request Timeout"
|
||||
case StatusConflict:
|
||||
return "Conflict"
|
||||
case StatusGone:
|
||||
return "Gone"
|
||||
case StatusLengthRequired:
|
||||
return "Length Required"
|
||||
case StatusPreconditionFailed:
|
||||
return "Precondition Failed"
|
||||
case StatusRequestEntityTooLarge:
|
||||
return "Request Entity Too Large"
|
||||
case StatusRequestURITooLong:
|
||||
return "Request URI Too Long"
|
||||
case StatusUnsupportedMediaType:
|
||||
return "Unsupported Media Type"
|
||||
case StatusRequestedRangeNotSatisfiable:
|
||||
return "Requested Range Not Satisfiable"
|
||||
case StatusExpectationFailed:
|
||||
return "Expectation Failed"
|
||||
case StatusTeapot:
|
||||
return "I'm a teapot"
|
||||
case StatusMisdirectedRequest:
|
||||
return "Misdirected Request"
|
||||
case StatusUnprocessableEntity:
|
||||
return "Unprocessable Entity"
|
||||
case StatusLocked:
|
||||
return "Locked"
|
||||
case StatusFailedDependency:
|
||||
return "Failed Dependency"
|
||||
case StatusTooEarly:
|
||||
return "Too Early"
|
||||
case StatusUpgradeRequired:
|
||||
return "Upgrade Required"
|
||||
case StatusPreconditionRequired:
|
||||
return "Precondition Required"
|
||||
case StatusTooManyRequests:
|
||||
return "Too Many Requests"
|
||||
case StatusRequestHeaderFieldsTooLarge:
|
||||
return "Request Header Fields Too Large"
|
||||
case StatusUnavailableForLegalReasons:
|
||||
return "Unavailable For Legal Reasons"
|
||||
case StatusInternalServerError:
|
||||
return "Internal Server Error"
|
||||
case StatusNotImplemented:
|
||||
return "Not Implemented"
|
||||
case StatusBadGateway:
|
||||
return "Bad Gateway"
|
||||
case StatusServiceUnavailable:
|
||||
return "Service Unavailable"
|
||||
case StatusGatewayTimeout:
|
||||
return "Gateway Timeout"
|
||||
case StatusHTTPVersionNotSupported:
|
||||
return "HTTP Version Not Supported"
|
||||
case StatusVariantAlsoNegotiates:
|
||||
return "Variant Also Negotiates"
|
||||
case StatusInsufficientStorage:
|
||||
return "Insufficient Storage"
|
||||
case StatusLoopDetected:
|
||||
return "Loop Detected"
|
||||
case StatusNotExtended:
|
||||
return "Not Extended"
|
||||
case StatusNetworkAuthenticationRequired:
|
||||
return "Network Authentication Required"
|
||||
default:
|
||||
return "INVALID STATUS CODE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package http
|
||||
|
||||
type HttpError struct {
|
||||
Status StatusCode
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *HttpError) Error() string {
|
||||
return e.Status.String() + ": " + e.Msg
|
||||
}
|
||||
|
||||
func NewHttpError(code StatusCode, msg string) *HttpError {
|
||||
return &HttpError{Status: code, Msg: msg}
|
||||
}
|
||||
Reference in New Issue
Block a user