package web import ( "bufio" "bytes" "io" "log" "net" "os" "os/signal" "strconv" "strings" "sync" "syscall" router "git.sharkk.net/Go/Router" ) // Pre-allocated response components var ( // Common strings for response headers contentLengthHeader = []byte(HeaderContentLength + ": ") crlf = []byte("\r\n") colonSpace = []byte(": ") // Status line map for quick lookups statusLines = map[int][]byte{ 100: []byte("HTTP/1.1 100 Continue\r\n"), 101: []byte("HTTP/1.1 101 Switching Protocols\r\n"), 200: []byte("HTTP/1.1 200 OK\r\n"), 201: []byte("HTTP/1.1 201 Created\r\n"), 202: []byte("HTTP/1.1 202 Accepted\r\n"), 204: []byte("HTTP/1.1 204 No Content\r\n"), 206: []byte("HTTP/1.1 206 Partial Content\r\n"), 300: []byte("HTTP/1.1 300 Multiple Choices\r\n"), 301: []byte("HTTP/1.1 301 Moved Permanently\r\n"), 302: []byte("HTTP/1.1 302 Found\r\n"), 304: []byte("HTTP/1.1 304 Not Modified\r\n"), 307: []byte("HTTP/1.1 307 Temporary Redirect\r\n"), 308: []byte("HTTP/1.1 308 Permanent Redirect\r\n"), 400: []byte("HTTP/1.1 400 Bad Request\r\n"), 401: []byte("HTTP/1.1 401 Unauthorized\r\n"), 403: []byte("HTTP/1.1 403 Forbidden\r\n"), 404: []byte("HTTP/1.1 404 Not Found\r\n"), 405: []byte("HTTP/1.1 405 Method Not Allowed\r\n"), 406: []byte("HTTP/1.1 406 Not Acceptable\r\n"), 409: []byte("HTTP/1.1 409 Conflict\r\n"), 410: []byte("HTTP/1.1 410 Gone\r\n"), 412: []byte("HTTP/1.1 412 Precondition Failed\r\n"), 413: []byte("HTTP/1.1 413 Payload Too Large\r\n"), 415: []byte("HTTP/1.1 415 Unsupported Media Type\r\n"), 416: []byte("HTTP/1.1 416 Range Not Satisfiable\r\n"), 429: []byte("HTTP/1.1 429 Too Many Requests\r\n"), 500: []byte("HTTP/1.1 500 Internal Server Error\r\n"), 501: []byte("HTTP/1.1 501 Not Implemented\r\n"), 502: []byte("HTTP/1.1 502 Bad Gateway\r\n"), 503: []byte("HTTP/1.1 503 Service Unavailable\r\n"), 504: []byte("HTTP/1.1 504 Gateway Timeout\r\n"), } ) // Interface for an HTTP server. type Server interface { Get(path string, handler Handler) Post(path string, handler Handler) Put(path string, handler Handler) Delete(path string, handler Handler) Patch(path string, handler Handler) Request(method string, path string, headers []Header, body io.Reader) Response Router() *router.Router[Handler] Run(address string) error Use(handlers ...Handler) } // HTTP server. type server struct { handlers []Handler contextPool sync.Pool bufferPool sync.Pool router *router.Router[Handler] errorHandler func(Context, error) } // Creates a new HTTP server. func NewServer() Server { r := &router.Router[Handler]{} s := &server{ router: r, handlers: []Handler{ func(c Context) error { ctx := c.(*context) handler := r.LookupNoAlloc(ctx.request.method, ctx.request.path, ctx.request.addParameter) if handler == nil { ctx.SetStatus(404) return nil } return handler(c) }, }, errorHandler: func(ctx Context, err error) { log.Println(ctx.Request().Path(), err) }, } s.contextPool.New = func() any { return s.newContext() } s.bufferPool.New = func() any { return bytes.NewBuffer(make([]byte, 0, 1024)) } return s } // Registers a handler for the given GET path. func (s *server) Get(path string, handler Handler) { s.Router().Add("GET", path, handler) } // Registers a handler for the given POST path. func (s *server) Post(path string, handler Handler) { s.Router().Add("POST", path, handler) } // Registers a handler for the given PUT path. func (s *server) Put(path string, handler Handler) { s.Router().Add("PUT", path, handler) } // Registers a handler for the given DELETE path. func (s *server) Delete(path string, handler Handler) { s.Router().Add("DELETE", path, handler) } // Registers a handler for the given PATCH path. func (s *server) Patch(path string, handler Handler) { s.Router().Add("PATCH", path, handler) } // Performs a synthetic request and returns the response. // This function keeps the response in memory so it's slightly slower than a real request. // However it is very useful inside tests where you don't want to spin up a real web server. func (s *server) Request(method string, url string, headers []Header, body io.Reader) Response { ctx := s.newContext() ctx.request.headers = headers s.handleRequest(ctx, method, url, io.Discard) return ctx.Response() } // Starts the server on the given address. func (s *server) Run(address string) error { listener, err := net.Listen("tcp", address) if err != nil { return err } defer listener.Close() go func() { for { conn, err := listener.Accept() if err != nil { continue } go s.handleConnection(conn) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop return nil } // Returns the router used by the server. func (s *server) Router() *router.Router[Handler] { return s.router } // Adds handlers to your handlers chain. func (s *server) Use(handlers ...Handler) { last := s.handlers[len(s.handlers)-1] s.handlers = append(s.handlers[:len(s.handlers)-1], handlers...) s.handlers = append(s.handlers, last) } // Handles an accepted connection. func (s *server) handleConnection(conn net.Conn) { var ( ctx = s.contextPool.Get().(*context) method string url string ) ctx.reader.Reset(conn) defer conn.Close() defer s.contextPool.Put(ctx) for { // Read the HTTP request line message, err := ctx.reader.ReadString('\n') if err != nil { return } space := strings.IndexByte(message, ' ') if space <= 0 { io.WriteString(conn, "HTTP/1.1 400 Bad Request\r\n\r\n") return } method = message[:space] if !isRequestMethod(method) { io.WriteString(conn, "HTTP/1.1 400 Bad Request\r\n\r\n") return } lastSpace := strings.LastIndexByte(message, ' ') if lastSpace == space { lastSpace = len(message) - len("\r\n") } url = message[space+1 : lastSpace] // Add headers until we meet an empty line ctx.request.headers = ctx.request.headers[:0] // Reset headers without allocation for { message, err = ctx.reader.ReadString('\n') if err != nil { return } if message == "\r\n" { break } colon := strings.IndexByte(message, ':') if colon <= 0 { continue } key := message[:colon] value := message[colon+2 : len(message)-2] ctx.request.headers = append(ctx.request.headers, Header{ Key: key, Value: value, }) } // Read the body, if any if contentLength := ctx.request.Header(HeaderContentLength); contentLength != "" { length, _ := strconv.Atoi(contentLength) if cap(ctx.request.body) >= length { ctx.request.body = ctx.request.body[:length] // Reuse existing slice if possible } else { ctx.request.body = make([]byte, length) } ctx.reader.Read(ctx.request.body) } else { ctx.request.body = ctx.request.body[:0] // Empty the body slice without allocation } // Handle the request s.handleRequest(ctx, method, url, conn) // Clean up the context - reset slices without allocation ctx.request.body = ctx.request.body[:0] ctx.response.headers = ctx.response.headers[:0] ctx.response.body = ctx.response.body[:0] ctx.params = ctx.params[:0] ctx.handlerCount = 0 ctx.status = 200 } } // Handles the given request with reduced allocations. func (s *server) handleRequest(ctx *context, method string, url string, writer io.Writer) { ctx.method = method ctx.scheme, ctx.host, ctx.path, ctx.query = parseURL(url) err := s.handlers[0](ctx) if err != nil { s.errorHandler(ctx, err) } // Get buffer from pool buf := s.bufferPool.Get().(*bytes.Buffer) buf.Reset() defer s.bufferPool.Put(buf) // Write status line using map lookup for efficiency if statusLine, ok := statusLines[int(ctx.status)]; ok { buf.Write(statusLine) } else { // For uncommon status codes, format the line dynamically buf.WriteString("HTTP/1.1 ") buf.WriteString(strconv.Itoa(int(ctx.status))) buf.Write(crlf) } // Write Content-Length header buf.Write(contentLengthHeader) buf.WriteString(strconv.Itoa(len(ctx.response.body))) buf.Write(crlf) // Write all response headers for _, header := range ctx.response.headers { buf.WriteString(header.Key) buf.Write(colonSpace) buf.WriteString(header.Value) buf.Write(crlf) } // End headers buf.Write(crlf) // Write headers writer.Write(buf.Bytes()) // Write body directly to avoid another copy if len(ctx.response.body) > 0 { writer.Write(ctx.response.body) } } // Allocates a new context with the default state. func (s *server) newContext() *context { return &context{ server: s, request: request{ reader: bufio.NewReader(nil), body: make([]byte, 0, 1024), headers: make([]Header, 0, 8), params: make([]router.Parameter, 0, 8), }, response: response{ body: make([]byte, 0, 1024), headers: make([]Header, 0, 8), status: 200, }, } }