mirror of
https://github.com/soypat/lneto.git
synced 2026-08-06 07:53:41 +00:00
181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
package httphi
|
|
|
|
import (
|
|
"strings"
|
|
"unsafe"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/internal"
|
|
)
|
|
|
|
// Handle is a extremely low-level HTTP handling method used internally in [Router].
|
|
// Requires exchange to be acquired and configured. Will panic if any argument is nil.
|
|
// Handle does not close the connection on any outcome: the caller owns it.
|
|
func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
|
reqhdr := &exch.reqHdr
|
|
reqhdr.Reset(nil)
|
|
var consecutiveBackoffs uint
|
|
for {
|
|
n, err := reqhdr.ReadFromLimited(exch.rw, reqhdr.BufferFree())
|
|
if err != nil {
|
|
exch.readErr = err
|
|
return err
|
|
} else if n == 0 {
|
|
backoff.Do(consecutiveBackoffs)
|
|
consecutiveBackoffs++
|
|
continue
|
|
}
|
|
consecutiveBackoffs = 0
|
|
const asRequest = false
|
|
needMore, err := reqhdr.TryParse(asRequest)
|
|
if needMore {
|
|
continue // Request header split across reads, accumulate the rest.
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
break // Done!
|
|
}
|
|
// Setup Exchange fields necessary for correct functioning.
|
|
parsed := reqhdr.BufferParsed()
|
|
exch.respRemains = reqhdr.BufferReceived() - parsed
|
|
exch.respHeaderOff = uint16(parsed)
|
|
exch.respHeaderLen = 0
|
|
if len(reqhdr.Protocol()) == 0 {
|
|
// Request line with no HTTP version is a HTTP/0.9 simple-request, which
|
|
// httpraw tolerates. It is not a valid HTTP/1.1 request-line, RFC 9112 3.
|
|
exch.WriteHeader(int(StatusBadRequest))
|
|
return errNoRequestProto
|
|
}
|
|
// Mux on the request path: the query string is the handler's business.
|
|
path := reqhdr.RequestPath()
|
|
meth := reqhdr.Method()
|
|
handler := mux.LookupHandler(MethodFromBytes(meth), path)
|
|
if handler != nil {
|
|
handler(exch)
|
|
exch.FlushHeader()
|
|
} else {
|
|
exch.WriteHeader(404)
|
|
}
|
|
// TODO write response from exchange here.
|
|
return nil
|
|
}
|
|
|
|
// HandlerFunc serves a single request, playing the part of http.Handler.
|
|
// The exchange is only valid for the duration of the call: it is released to
|
|
// the router's pool on return, so a handler must not retain it nor any slice it
|
|
// handed out.
|
|
type HandlerFunc func(ex *Exchange)
|
|
|
|
// Mux resolves a request to the handler that serves it. [Handle] calls
|
|
// LookupHandler with the request-target's path, not the whole target, and
|
|
// replies 404 when it returns nil.
|
|
type Mux interface {
|
|
LookupHandler(get Method, uri []byte) HandlerFunc
|
|
}
|
|
|
|
// MuxSlice is a [Mux] backed by a slice of registered endpoints, matched by
|
|
// exact path. Lookup is linear in the number of registrations.
|
|
type MuxSlice struct {
|
|
// TODO: binary search worth it?
|
|
_handlers []struct {
|
|
method Method
|
|
path string
|
|
handler HandlerFunc
|
|
}
|
|
}
|
|
|
|
// Reset discards all registered handlers, reusing the backing array and growing
|
|
// it to fit capacity registrations.
|
|
func (sm *MuxSlice) Reset(capacity int) {
|
|
internal.SliceReuse(&sm._handlers, capacity)
|
|
}
|
|
|
|
// LookupHandler returns the handler registered for request path, or nil if none matches.
|
|
// The first registration matching both method and uri wins.
|
|
func (sm *MuxSlice) LookupHandler(method Method, path []byte) HandlerFunc {
|
|
for _, endpoint := range sm._handlers {
|
|
if endpoint.method != MethUndefined && endpoint.method != method {
|
|
continue
|
|
}
|
|
// Method matches.
|
|
if b2s(path) == endpoint.path {
|
|
return endpoint.handler
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handle registers handler for reg, either a bare path matching any method or a
|
|
// method and path separated by a space, i.e: "/health" or "GET /health".
|
|
// Handle does not check for duplicate registrations: the first one added wins.
|
|
func (sm *MuxSlice) Handle(optMethodAndPath string, handler HandlerFunc) {
|
|
v := internal.SliceReclaim(&sm._handlers)
|
|
method := MethUndefined
|
|
methodOrURL, url, methodFound := strings.Cut(optMethodAndPath, " ")
|
|
if methodFound {
|
|
method = MethodFromBytes([]byte(methodOrURL))
|
|
} else {
|
|
url = methodOrURL
|
|
}
|
|
v.method = method
|
|
v.path = url
|
|
v.handler = handler
|
|
}
|
|
|
|
// Method is a HTTP request method, parsed by [MethodFromBytes].
|
|
type Method uint8
|
|
|
|
const (
|
|
MethUndefined Method = iota // undefined
|
|
MethGet // GET
|
|
// lol.
|
|
MethHead // HEAD
|
|
MethPost // POST
|
|
MethPut // PUT
|
|
// RFC 5789
|
|
MethPatch // PATCH
|
|
MethDelete // DELETE
|
|
MethConnect // CONNECT
|
|
MethOptions // OPTIONS
|
|
MethTrace // TRACE
|
|
MethUnknown // unknown
|
|
)
|
|
|
|
// MethodFromBytes returns the [Method] matching meth, [MethUndefined] if meth is
|
|
// empty and [MethUnknown] if it names a method this package does not know.
|
|
// Comparison is case sensitive: methods are uppercase, RFC 9110 9.1.
|
|
func MethodFromBytes(meth []byte) (res Method) {
|
|
if len(meth) == 0 {
|
|
return MethUndefined
|
|
}
|
|
switch unsafe.String(&meth[0], len(meth)) {
|
|
case "GET":
|
|
res = MethGet
|
|
case "HEAD":
|
|
res = MethHead
|
|
case "POST":
|
|
res = MethPost
|
|
case "PUT":
|
|
res = MethPut
|
|
case "PATCH":
|
|
res = MethPatch
|
|
case "DELETE":
|
|
res = MethDelete
|
|
case "CONNECT":
|
|
res = MethConnect
|
|
case "OPTIONS":
|
|
res = MethOptions
|
|
case "TRACE":
|
|
res = MethTrace
|
|
default:
|
|
res = MethUnknown
|
|
}
|
|
return res
|
|
}
|
|
|
|
// b2s converts byte slice to a string without memory allocation.
|
|
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
|
func b2s(b []byte) string {
|
|
return unsafe.String(unsafe.SliceData(b), len(b))
|
|
}
|