package http
- License: MIT
- Repository: https://github.com/laytan/odin-http/blob/main
Index
Types (31)
- Body
- Body_Callback
- Body_Error
- Connection
- Connection_State
- Cookie
- Cookie_Same_Site
- Handle_Proc
- Handler
- Handler_Proc
- Headers
- Method
- Mime_Type
- Query_Entry
- Rate_Limit_Data
- Rate_Limit_On_Limit
- Rate_Limit_Opts
- Request
- Requestline
- Requestline_Error
- Response
- Response_Writer
- Route
- Router
- Server
- Server_Opts
- Server_State
- Server_Thread
- Status
- URL
- Version
Constants (0)
This section is empty.
Variables (3)
Procedures (94)
- body
- body_error_status
- body_set_bytes
- body_set_str
- body_url_encoded
- cookie_date_parse
- cookie_parse
- cookie_string
- cookie_write
- date_parse
- date_string
- date_write
- handler
- header_allowed_trailer
- header_parse
- headers_count
- headers_delete
- headers_delete_unsafe
- headers_get
- headers_get_unsafe
- headers_has
- headers_has_unsafe
- headers_init
- headers_set
- headers_set_close
- headers_set_content_type_mime
- headers_set_content_type_string
- headers_set_unsafe
- headers_validate
- headers_validate_for_server
- listen
- listen_and_serve
- method_parse
- method_string
- middleware_proc
- mime_from_extension
- mime_to_content_type
- query_get
- query_get_bool
- query_get_int
- query_get_percent_decoded
- query_get_uint
- query_iter
- rate_limit
- rate_limit_destroy
- rate_limit_message
- request_cookie_get
- request_cookies
- request_cookies_iter
- request_init
- request_path
- request_path_write
- requestline_parse
- requestline_write
- respond_dir
- respond_file
- respond_file_content
- respond_html
- respond_json
- respond_plain
- respond_with_none
- respond_with_status
- response_init
- response_status
- response_writer_init
- route_all
- route_connect
- route_delete
- route_get
- route_head
- route_options
- route_patch
- route_post
- route_put
- route_trace
- router_destroy
- router_handler
- router_init
- serve
- server_shutdown
- server_shutdown_on_interrupt
- status_from_string
- status_is_client_error
- status_is_informational
- status_is_redirect
- status_is_server_error
- status_is_success
- status_string
- status_valid
- test_dynamic_unwritten
- url_parse
- version_parse
- version_string
- version_write
Procedure Groups (3)
Types
Body_Callback ¶
Body_Callback :: proc(user_data: rawptr, body: string, err: bufio.Scanner_Error)
Related Procedures With Parameters
Body_Error ¶
Body_Error :: bufio.Scanner_Error
Connection ¶
Connection :: struct { server: ^Server, socket: net.TCP_Socket, state: Connection_State, scanner: Scanner, temp_allocator: mem_virtual.Arena, loop: Loop, }
TODO/PERF: pool the connections, saves having to allocate scanner buf and temp_allocator every time.
Connection_State ¶
Connection_State :: enum int { Pending, // Pending a client to attach. New, // Got client, waiting to service first request. Active, // Servicing request. Idle, // Waiting for next request. Will_Close, // Closing after the current response is sent. Closing, // Going to close, cleaning up. Closed, // Fully closed. }
Cookie ¶
Cookie :: struct { name: string, value: string, domain: runtime.Maybe($T=string), expires_gmt: runtime.Maybe($T=Time), max_age_secs: runtime.Maybe($T=int), path: runtime.Maybe($T=string), http_only: bool, partitioned: bool, secure: bool, same_site: Cookie_Same_Site, }
Related Procedures With Parameters
Related Procedures With Returns
Cookie_Same_Site ¶
Cookie_Same_Site :: enum int { Unspecified, None, Strict, Lax, }
Handle_Proc ¶
Related Procedures With Parameters
Handler ¶
Handler :: struct { user_data: rawptr, next: runtime.Maybe($T=^Handler), handle: Handler_Proc, }
Related Procedures With Parameters
- listen_and_serve
- rate_limit
- route_all
- route_connect
- route_delete
- route_get
- route_head
- route_options
- route_patch
- route_post
- route_put
- route_trace
- serve
Related Procedures With Returns
Handler_Proc ¶
Related Procedures With Parameters
Headers ¶
A case-insensitive ASCII map for storing headers.
Related Procedures With Parameters
- header_parse
- headers_count
- headers_delete
- headers_delete_unsafe
- headers_get
- headers_get_unsafe
- headers_has
- headers_has_unsafe
- headers_init
- headers_set
- headers_set_close
- headers_set_content_type_mime
- headers_set_content_type_string
- headers_set_unsafe
- headers_validate
- headers_validate_for_server
- headers_set_content_type (procedure groups)
Method ¶
Method :: enum int { Get, Post, Delete, Patch, Put, Head, Connect, Options, Trace, }
Related Procedures With Parameters
Related Procedures With Returns
Mime_Type ¶
Mime_Type :: enum int { Plain, Css, Csv, Gif, Html, Ico, Jpeg, Js, Json, Png, Svg, Url_Encoded, Xml, Zip, Wasm, }
Related Procedures With Parameters
- headers_set_content_type_mime
- mime_to_content_type
- headers_set_content_type (procedure groups)
Related Procedures With Returns
Query_Entry ¶
Related Procedures With Returns
Rate_Limit_Data ¶
Rate_Limit_Data :: struct { opts: ^Rate_Limit_Opts, next_sweep: time.Time, hits: map[net.Address]int, mu: sync.Mutex, }
Related Procedures With Parameters
Rate_Limit_On_Limit ¶
Rate_Limit_On_Limit :: struct { user_data: rawptr, on_limit: proc(req: ^Request, res: ^Response, user_data: rawptr), }
Related Procedures With Returns
Rate_Limit_Opts ¶
Rate_Limit_Opts :: struct { window: time.Duration, max: int, // Optional handler to call when a request is being rate-limited, allows you to customize the response. on_limit: runtime.Maybe($T=Rate_Limit_On_Limit), }
Related Procedures With Parameters
Request ¶
Request :: struct { // If in a handler, this is always there and never None. // TODO: we should not expose this as a maybe to package users. line: runtime.Maybe($T=Requestline), // Is true if the request is actually a HEAD request, // line.method will be .Get if Server_Opts.redirect_head_to_get is set. is_head: bool, headers: Headers, url: URL, client: net.Endpoint, // Route params/captures. url_params: []string, // Internal usage only. _scanner: ^Scanner, _body_ok: runtime.Maybe($T=bool), }
Related Procedures With Parameters
Requestline_Error ¶
Requestline_Error :: enum int { None, Method_Not_Implemented, Not_Enough_Fields, Invalid_Version_Format, }
Related Procedures With Returns
Response ¶
Response :: struct { // Add your headers and cookies here directly. headers: Headers, cookies: [dynamic]Cookie, // If the response has been sent. sent: bool, // NOTE: use `http.response_status` if the response body might have been set already. status: Status, // Only for internal usage. _conn: ^Connection, // TODO/PERF: with some internal refactoring, we should be able to write directly to the // connection (maybe a small buffer in this struct). _buf: bytes.Buffer, _heading_written: bool, }
Related Procedures With Parameters
- body_set_bytes
- body_set_str
- respond_dir
- respond_file
- respond_file_content
- respond_html
- respond_json
- respond_plain
- respond_with_none
- respond_with_status
- response_init
- response_status
- response_writer_init
- body_set (procedure groups)
- respond (procedure groups)
Response_Writer ¶
Response_Writer :: struct { r: ^Response, // The writer you can write to. w: io.Stream, // A dynamic wrapper over the `buffer` given in `response_writer_init`, doesn't allocate. buf: [dynamic]u8, // If destroy or close has been called. ended: bool, }
Related Procedures With Parameters
Router ¶
Router :: struct { allocator: runtime.Allocator, routes: map[Method][dynamic]Route, all: [dynamic]Route, }
Related Procedures With Parameters
Server ¶
Server :: struct { opts: Server_Opts, tcp_sock: net.TCP_Socket, conn_allocator: runtime.Allocator, handler: Handler, main_thread: int, threads: []^thread.Thread, // Once the server starts closing/shutdown this is set to true, all threads will check it // and start their thread local shutdown procedure. // // NOTE: This is only ever set from false to true, and checked repeatedly, // so it doesn't have to be atomic, this is purely to keep the thread sanitizer happy. closing: Atomic($T=bool), // Threads will decrement the wait group when they have fully closed/shutdown. // The main thread waits on this to clean up global data and return. threads_closed: sync.Wait_Group, // Updated every second with an updated date, this speeds up the server considerably // because it would otherwise need to call time.now() and format the date on each response. date: Server_Date, }
Related Procedures With Parameters
Server_Opts ¶
Server_Opts :: struct { // Whether the server should accept every request that sends a "Expect: 100-continue" header automatically. // Defaults to true. auto_expect_continue: bool, // When this is true, any HEAD request is automatically redirected to the handler as a GET request. // Then, when the response is sent, the body is removed from the response. // Defaults to true. redirect_head_to_get: bool, // Limit the maximum number of bytes to read for the request line (first line of request containing the URI). // The HTTP spec does not specify any limits but in practice it is safer. // RFC 7230 3.1.1 says: // Various ad hoc limitations on request-line length are found in // practice. It is RECOMMENDED that all HTTP senders and recipients // support, at a minimum, request-line lengths of 8000 octets. // defaults to 8000. limit_request_line: int, // Limit the length of the headers. // The HTTP spec does not specify any limits but in practice it is safer. // defaults to 8000. limit_headers: int, // The thread count to use, defaults to your core count - 1. thread_count: int, }
Related Procedures With Parameters
Server_State ¶
Server_State :: enum int { Uninitialized, Idle, Listening, Serving, Running, Closing, Cleaning, Closed, }
Server_Thread ¶
Server_Thread :: struct { conns: map[net.TCP_Socket]^Connection, state: Server_State, io: nbio._IO, }
Status ¶
Status :: enum int { Continue = 100, Switching_Protocols = 101, Processing = 102, Early_Hints = 103, OK = 200, Created = 201, Accepted = 202, Non_Authoritative_Information = 203, No_Content = 204, Reset_Content = 205, Partial_Content = 206, Multi_Status = 207, Already_Reported = 208, IM_Used = 226, Multiple_Choices = 300, Moved_Permanently = 301, Found = 302, See_Other = 303, Not_Modified = 304, Use_Proxy = 305, // Deprecated. Unused = 306, // Deprecated. Temporary_Redirect = 307, Permanent_Redirect = 308, Bad_Request = 400, Unauthorized = 401, Payment_Required = 402, Forbidden = 403, Not_Found = 404, Method_Not_Allowed = 405, Not_Acceptable = 406, Proxy_Authentication_Required = 407, Request_Timeout = 408, Conflict = 409, Gone = 410, Length_Required = 411, Precondition_Failed = 412, Payload_Too_Large = 413, URI_Too_Long = 414, Unsupported_Media_Type = 415, Range_Not_Satisfiable = 416, Expectation_Failed = 417, Im_A_Teapot = 418, Misdirected_Request = 421, Unprocessable_Content = 422, Locked = 423, Failed_Dependency = 424, Too_Early = 425, Upgrade_Required = 426, Precondition_Required = 428, Too_Many_Requests = 429, Request_Header_Fields_Too_Large = 431, Unavailable_For_Legal_Reasons = 451, Internal_Server_Error = 500, Not_Implemented = 501, Bad_Gateway = 502, Service_Unavailable = 503, Gateway_Timeout = 504, HTTP_Version_Not_Supported = 505, Variant_Also_Negotiates = 506, Insufficient_Storage = 507, Loop_Detected = 508, Not_Extended = 510, Network_Authentication_Required = 511, }
Related Procedures With Parameters
- respond_file_content
- respond_html
- respond_json
- respond_plain
- respond_with_status
- response_status
- status_is_client_error
- status_is_informational
- status_is_redirect
- status_is_server_error
- status_is_success
- status_string
- status_valid
- respond (procedure groups)
Related Procedures With Returns
URL ¶
URL :: struct { raw: string, // All other fields are views/slices into this string. scheme: string, host: string, path: string, query: string, }
Related Procedures With Parameters
- query_get
- query_get_bool
- query_get_int
- query_get_percent_decoded
- query_get_uint
- request_path
- request_path_write
Related Procedures With Returns
Constants
This section is empty.
Variables
Default_Endpoint ¶
Default_Endpoint: net.Endpoint = …
Default_Server_Opts ¶
Default_Server_Opts: Server_Opts = …
td ¶
@(thread_local) td: Server_Thread
Procedures
body ¶
body :: proc(req: ^Request, max_length: int = -1, user_data: rawptr, cb: Body_Callback) {…}
Retrieves the request's body.
If the request has the chunked Transfer-Encoding header set, the chunks are all read and returned. Otherwise, the Content-Length header is used to determine what to read and return it.
max_length
can be used to set a maximum amount of bytes we try to read, once it goes over this,
an error is returned.
Do not call this more than once.
Tip If an error is returned, easily respond with an appropriate error code like this, http.respond(res, http.body_error_status(err))
.
body_error_status ¶
body_error_status :: proc(e: bufio.Scanner_Error) -> Status {…}
Returns an appropriate status code for the given body error.
body_set_bytes ¶
body_set_bytes :: proc(r: ^Response, byts: []u8, loc := #caller_location) {…}
Prefer the procedure group body_set
.
body_set_str ¶
body_set_str :: proc(r: ^Response, str: string, loc := #caller_location) {…}
Prefer the procedure group body_set
.
body_url_encoded ¶
body_url_encoded :: proc(plain: string, allocator := context.temp_allocator) -> (res: map[string]string, ok: bool) {…}
Parses a URL encoded body, aka bodies with the 'Content-Type: application/x-www-form-urlencoded'.
Key&value pairs are percent decoded and put in a map.
cookie_date_parse ¶
Implementation of the algorithm described in RFC 6265 section 5.1.1.
cookie_parse ¶
cookie_parse :: proc(value: string, allocator := context.allocator) -> (cookie: Cookie, ok: bool) {…}
TODO: check specific whitespace requirements in RFC.
Allocations are done to check case-insensitive attributes but they are deleted right after. So, all the returned strings (inside cookie) are slices into the given value string.
cookie_string ¶
cookie_string :: proc(c: Cookie, allocator := context.allocator) -> string {…}
Builds the Set-Cookie header string representation of the given cookie.
cookie_write ¶
Builds the Set-Cookie header string representation of the given cookie.
date_string ¶
date_string :: proc(t: time.Time, allocator := context.allocator) -> string {…}
Formats a time in the HTTP header format (no timezone conversion is done, GMT expected):
<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
date_write ¶
Formats a time in the HTTP header format (no timezone conversion is done, GMT expected):
<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
handler ¶
handler :: proc(handle: Handle_Proc) -> Handler {…}
header_allowed_trailer ¶
Returns if this is a valid trailer header.
RFC 7230 4.1.2: A sender MUST NOT generate a trailer that contains a field necessary for message framing (e.g., Transfer-Encoding and Content-Length), routing (e.g., Host), request modifiers (e.g., controls and conditionals in Section 5 of [RFC7231]), authentication (e.g., see [RFC7235] and [RFC6265]), response control data (e.g., see Section 7.1 of [RFC7231]), or determining how to process the payload (e.g., Content-Encoding, Content-Type, Content-Range, and Trailer).
header_parse ¶
header_parse :: proc(headers: ^Headers, line: string, allocator := context.temp_allocator) -> (key: string, ok: bool) {…}
Parses the header and adds it to the headers if valid. The given string is copied.
headers_delete_unsafe ¶
Unsafely delete a header, given key is assumed to be a lowercase string.
headers_get_unsafe ¶
Unsafely get header, given key is assumed to be a lowercase string.
headers_has_unsafe ¶
Unsafely check for a header, given key is assumed to be a lowercase string.
headers_init ¶
headers_init :: proc(h: ^Headers, allocator := context.temp_allocator) {…}
headers_set ¶
headers_set :: proc(h: ^Headers, k: string, v: string, loc := #caller_location) -> string {…}
Sets a header, given key is first sanitized, final (sanitized) key is returned.
headers_set_close ¶
headers_set_close :: proc(h: ^Headers) {…}
headers_set_unsafe ¶
headers_set_unsafe :: proc(h: ^Headers, k: string, v: string, loc := #caller_location) {…}
Unsafely set header, given key is assumed to be a lowercase string and to be without newlines.
headers_validate ¶
Validates the headers, use headers_validate_for_server
if these are request headers
that should be validated from the server side.
headers_validate_for_server ¶
Validates the headers of a request, from the pov of the server.
listen ¶
listen :: proc(s: ^Server, endpoint: net.Endpoint = Default_Endpoint, opts: Server_Opts = Default_Server_Opts) -> (err: net.Network_Error) {…}
listen_and_serve ¶
listen_and_serve :: proc(s: ^Server, h: Handler, endpoint: net.Endpoint = Default_Endpoint, opts: Server_Opts = Default_Server_Opts) -> (err: net.Network_Error) {…}
middleware_proc ¶
middleware_proc :: proc(next: runtime.Maybe($T=^Handler), handle: Handler_Proc) -> Handler {…}
query_get_percent_decoded ¶
query_get_percent_decoded :: proc(url: URL, key: string, allocator := context.temp_allocator) -> (val: string, ok: bool) {…}
query_iter ¶
query_iter :: proc(query: ^string) -> (entry: Query_Entry, ok: bool) {…}
rate_limit ¶
rate_limit :: proc(data: ^Rate_Limit_Data, next: ^Handler, opts: ^Rate_Limit_Opts, allocator := context.allocator) -> Handler {…}
Basic rate limit based on IP address.
rate_limit_destroy ¶
rate_limit_destroy :: proc(data: ^Rate_Limit_Data) {…}
rate_limit_message ¶
rate_limit_message :: proc(message: ^string) -> Rate_Limit_On_Limit {…}
Convenience method to create a Rate_Limit_On_Limit that writes the given message.
request_cookie_get ¶
Retrieves the cookie with the given key
out of the requests Cookie
header.
If the same key is in the header multiple times the last one is returned.
request_cookies ¶
request_cookies :: proc(r: ^Request, allocator := context.temp_allocator) -> (res: map[string]string) {…}
Allocates a map with the given allocator and puts all cookie pairs from the requests Cookie
header into it.
If the same key is in the header multiple times the last one is returned.
request_cookies_iter ¶
Iterates the cookies from right to left.
request_init ¶
request_init :: proc(r: ^Request, allocator := context.temp_allocator) {…}
request_path ¶
request_path :: proc(target: URL, allocator := context.allocator) -> (rq_path: string) {…}
requestline_parse ¶
requestline_parse :: proc(s: string, allocator := context.temp_allocator) -> (line: Requestline, err: Requestline_Error) {…}
A request-line begins with a method token, followed by a single space (SP), the request-target, another single space (SP), the protocol version, and ends with CRLF.
This allocates a clone of the target, because this is intended to be used with a scanner, which has a buffer that changes every read.
requestline_write ¶
requestline_write :: proc(w: io.Stream, rline: Requestline) -> io.Error {…}
respond_dir ¶
respond_dir :: proc(r: ^Response, base, target, request: string, loc := #caller_location) {…}
Sets the response to one that, based on the request path, returns a file. base: The base of the request path that should be removed when retrieving the file. target: The path to the directory to serve. request: The request path.
Path traversal is detected and cleaned up. The Content-Type is set based on the file extension, see the MimeType enum for known file extensions.
respond_file ¶
respond_file :: proc(r: ^Response, path: string, content_type: runtime.Maybe($T=Mime_Type) = nil, loc := #caller_location) {…}
Sends the content of the file at the given path as the response.
This procedure uses non blocking IO and only allocates the size of the file in the body's buffer, no other allocations or temporary buffers, this is to make it as fast as possible.
The content type is taken from the path, optionally overwritten using the parameter.
If the file doesn't exist, a 404 response is sent. If any other error occurs, a 500 is sent and the error is logged.
respond_file_content ¶
respond_file_content :: proc(r: ^Response, path: string, content: []u8, status: Status = .OK, loc := #caller_location) {…}
Responds with the given content, determining content type from the given path.
This is very useful when you want to #load(path)
at compile time and respond with that.
respond_html ¶
respond_html :: proc(r: ^Response, html: string, status: Status = .OK, loc := #caller_location) {…}
Sets the response to one that sends the given HTML.
respond_json ¶
respond_json :: proc(r: ^Response, v: any, status: Status = .OK, opt: encoding_json.Marshal_Options = {}, loc := #caller_location) -> (err: encoding_json.Marshal_Error) {…}
Sets the response to one that returns the JSON representation of the given value.
respond_plain ¶
respond_plain :: proc(r: ^Response, text: string, status: Status = .OK, loc := #caller_location) {…}
Sets the response to one that sends the given plain text.
respond_with_none ¶
respond_with_none :: proc(r: ^Response, loc := #caller_location) {…}
Prefer the procedure group respond
.
respond_with_status ¶
respond_with_status :: proc(r: ^Response, status: Status, loc := #caller_location) {…}
Prefer the procedure group respond
.
response_init ¶
response_init :: proc(r: ^Response, allocator := context.allocator) {…}
response_status ¶
Sets the status code with the safety of being able to do this after writing (part of) the body.
response_writer_init ¶
response_writer_init :: proc(rw: ^Response_Writer, r: ^Response, buffer: []u8) -> io.Stream {…}
Initialize a writer you can use to write responses. Use the body_set
procedure group if you have
a string or byte slice.
The buffer can be used to avoid very small writes, like the ones when you use the json package (each write in the json package is only a few bytes). You are allowed to pass nil which will disable buffering.
NOTE: You need to call io.destroy to signal the end of the body, OR io.close to send the response.
route_all ¶
Adds a catch-all fallback route (all methods, ran if no other routes match).
route_head ¶
NOTE: this does not get called when Server_Opts.redirect_head_to_get
is set to true.
router_destroy ¶
router_destroy :: proc(router: ^Router) {…}
router_handler ¶
Returns a handler that matches against the given routes.
router_init ¶
router_init :: proc(router: ^Router, allocator := context.allocator) {…}
serve ¶
serve :: proc(s: ^Server, h: Handler) -> (err: net.Network_Error) {…}
server_shutdown ¶
server_shutdown :: proc(s: ^Server) {…}
Starts a graceful shutdown.
Some error logs will be generated but all active connections are finished before closing them and all connections and threads are freed.
1. Stops 'server_start' from accepting new connections. 2. Close and free non-active connections. 3. Repeat 2 every SHUTDOWN_INTERVAL until no more connections are open. 4. Close the main socket. 5. Signal 'server_start' it can return.
server_shutdown_on_interrupt ¶
server_shutdown_on_interrupt :: proc(s: ^Server) {…}
Registers a signal handler to shutdown the server gracefully on interrupt signal. Can only be called once in the lifetime of the program because of a hacky interaction with libc.
test_dynamic_unwritten ¶
test_dynamic_unwritten :: proc(t: ^testing.T) {…}
version_parse ¶
Parses an HTTP version string according to RFC 7230, section 2.6.
version_string ¶
version_string :: proc(v: Version, allocator := context.allocator) -> string {…}
Procedure Groups
body_set ¶
body_set :: proc{ body_set_str, body_set_bytes, }
Sets the response body. After calling this you can no longer add headers to the response.
If, after calling, you want to change the status code, use the response_status
procedure.
For bodies where you do not know the size or want an io.Writer
, use the response_writer_init
procedure to create a writer.
headers_set_content_type ¶
headers_set_content_type :: proc{ headers_set_content_type_mime, headers_set_content_type_string, }
respond ¶
respond :: proc{ respond_with_none, respond_with_status, }
Sends the response back to the client, handlers should call this.
Source Files
- body.odin
- cookie.odin
- handlers.odin
- headers.odin
- http.odin
- mimes.odin
- request.odin
- response.odin
- responses.odin
- routing.odin
- scanner.odin
- server.odin
- status.odin
Generation Information
Generated with odin version dev-2024-10 (vendor "odin") Linux_amd64 @ 2024-10-30 12:34:52.771422134 +0000 UTC