w3

package module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 28, 2025 License: MIT Imports: 17 Imported by: 0

README

W3

Go Report Card Godoc Releases LICENSE

The web project provides two packages, web and router.

It is an evolution of github.com/ecnepsnai/web.

Important: This package does not make any API compatability gaurantees. I reserve the right to make breaking changes as needed.

W3

Package w3 is a modern HTTP server for modern go applications.

It is suitable for both front-end and back-end use, being able to deliver static content and act as a RESTful JSON server.

It includes simple controls to allow for user authentication with contextual data being available in every request, and provides simple per-user rate-limiting.

Router

Package router provides a simple & efficient parameterized HTTP router.

An HTTP router allows you to map an HTTP request method and path to a specific function. A parameterized HTTP router allows you to designate specific portions of the request path as a parameter, which can later be fetched during the request itself.

This package allows you modify the routing table ad-hoc, even while the server is running.

Documentation & Examples

For full documentation including examples please see the official package documentation

Documentation

Overview

Package w3 is a full-featured HTTP router and server for Go applications, suitable for serving static files, REST APIs, and more.

Web includes these features:

  • HTTP range support
  • Static file serving
  • Directory listings
  • Per-IP rate limiting
  • Per-request contextual data

Web offers four APIs for developers to choose from:

API

API provides everything you need to build poweful REST APIs using JSON. Define your routes and easily accept and return data as JSON.

Example:

router := w3.New("[::]:8080")
router.API.Get("/users", getUsers, options)
router.API.Get("/users/:username", getUsers, options)

For more information, see the documentation of w3.API.

HTTPEasy

HTTPEasy provides a straightforward interface to accept HTTP requets and return data.

Example:

router := w3.New("[::]:8080")
router.HTTPEasy.Get("/index.html", getIndex, options)
router.HTTPEasy.Get("/cat.jpg", getKitty, options)

For more information, see the documentation of w3.HTTPEasy.

HTTP

HTTP provides full access to the original HTTP request, allowing you total control over the response, whatever that may be.

Example:

router := w3.New("[::]:8080")
router.HTTP.Get("/index.html", getIndex, options)
router.HTTP.Get("/cat.jpg", getKitty, options)

For more information, see the documentation of w3.HTTP.

Example (Authentication)
package main

import (
	"net/http"
	"time"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	type User struct {
		Username string `json:"username"`
	}

	// Login
	loginHandle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		// Do any authentication logic here

		// Assuming the user authenticated successfully...
		response.Cookies = []http.Cookie{
			{
				Name:    "session",
				Value:   "1",
				Path:    "/",
				Expires: time.Now().AddDate(0, 0, 1),
			},
		}
		return true, nil
	}
	unauthenticatedOptions := &w3.HandleOptions{}
	server.API.GET("/login", loginHandle, unauthenticatedOptions)

	// Get User Info
	getUserHandle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		user := request.UserData.(User)
		return user, nil
	}

	authenticatedOptions := &w3.HandleOptions{
		// The authenticate method is where you validate that a request if from an authenticated, or simple "logged in"
		// user. In this example, we validate that a cookie is present.
		// Any data returned by this method is provided into the request handler as Request.UserData
		// Returning nil results in an HTTP 403 response
		AuthenticateMethod: func(request *http.Request) any {
			cookie, err := request.Cookie("session")
			if err != nil || cookie == nil {
				return nil
			}
			if cookie.Value != "1" {
				return nil
			}
			return map[string]string{
				"foo": "bar",
			}
		},
	}
	// Notice that we used a different HandleOptions instance with our AuthenticateMethod
	// an options without any AuthenticateMethod is considered unauthenticated
	server.API.GET("/user", getUserHandle, authenticatedOptions)

	if err := server.Start(); err != nil {
		panic(err)
	}
}
Example (File)
package main

import (
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		f, err := os.Open("/foo/bar")
		if err != nil {
			return &w3.HTTPResponse{
				Status: 500,
			}
		}
		return &w3.HTTPResponse{
			Reader: f,
		}
	}

	options := &w3.HandleOptions{}
	server.HTTPEasy.GET("/file", handle, options)

	if err := server.Start(); err != nil {
		panic(err)
	}
}
Example (Json)
package main

import (
	"time"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		return time.Now().Unix(), nil
	}

	options := &w3.HandleOptions{}
	server.API.GET("/time", handle, options)

	if err := server.Start(); err != nil {
		panic(err)
	}
}
Example (Ratelimit)
package main

import (
	"net/http"
	"time"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	// Restrict each connecting IP address to a maximum of 5 requests per second
	server.Options.MaxRequestsPerSecond = 5

	// Handle called when a request is rejected due to rate limiting
	server.SetRateLimitedHandle(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(429)
		w.Write([]byte("Too many requests"))
	})

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		return time.Now().Unix(), nil
	}

	options := &w3.HandleOptions{}
	server.API.GET("/time", handle, options)

	if err := server.Start(); err != nil {
		panic(err)
	}
}
Example (Telemetry)
package main

import (
	"time"

	"git.ecn.io/ian/w3"
	"git.ecn.io/ian/w3/router"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	server.SetPostRequestHandle(func(r *router.RequestResult) {
		_ = r.Method     // The HTTP method from the request
		_ = r.URL        // The URL from the request
		_ = r.Host       // The host from the request
		_ = r.RemoteAddr // The remote address from the request
		_ = r.Header     // The headers written to the response
		_ = r.StatusCode // The status code of the response
		_ = r.Elapsed    // The elapsed time that this request took
		_ = r.Panic      // The panic reason if a panic occured during the handle of the request
	})

	loginHandle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		// Assume this is a long-running task
		time.Sleep(5 * time.Second)
		return true, nil
	}
	server.API.GET("/example", loginHandle, &w3.HandleOptions{})

	if err := server.Start(); err != nil {
		panic(err)
	}
}
Example (Unixsocket)
package main

import (
	"net"
	"time"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	l, err := net.Listen("unix", "/example.socket")
	if err != nil {
		panic(err)
	}
	server := w3.NewListener(l, logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		return time.Now().Unix(), nil
	}

	options := &w3.HandleOptions{}
	server.API.GET("/time", handle, options)

	if err := server.Start(); err != nil {
		panic(err)
	}
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var CommonErrors = struct {
	NotFound        *Error
	BadRequest      *Error
	Unauthorized    *Error
	Forbidden       *Error
	ServerError     *Error
	TooManyRequests *Error
}{
	NotFound: &Error{
		Code:    404,
		Message: "Not Found",
	},
	BadRequest: &Error{
		Code:    400,
		Message: "Bad Request",
	},
	Unauthorized: &Error{
		Code:    403,
		Message: "Unauthorized",
	},
	Forbidden: &Error{
		Code:    403,
		Message: "Forbidden",
	},
	ServerError: &Error{
		Code:    500,
		Message: "Server Error",
	},
	TooManyRequests: &Error{
		Code:    429,
		Message: "Too Many Requests",
	},
}

CommonErrors are common errors types suitable for API endpoints

Default values for StaticOptions when nil is passed.

Functions

func MustURL added in v1.1.0

func MustURL(s string) *url.URL

MustURL is a convenience call to URL.Parse that panics if the URL is invalid. This should only be used in places where a hard-coded URL is being used.

func RealRemoteAddr

func RealRemoteAddr(r *http.Request) net.IP

RealRemoteAddr will try to get the real IP address of the incoming connection taking proxies into consideration. This function looks for the `X-Real-IP`, `X-Forwarded-For`, and `CF-Connecting-IP` headers, and if those don't exist will return the remote address of the connection.

Will never return nil, if it is unable to get a valid address it will return 0.0.0.0

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API describes a JSON API server. API handles return data or an error, and all responses are wrapped in a common response object; w3.JSONResponse.

func (API) DELETE

func (a API) DELETE(path string, handle APIHandle, options *HandleOptions)

DELETE register a new HTTP DELETE request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]

		return map[string]string{
			"username": username,
		}, nil
	}
	server.API.DELETE("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (API) GET

func (a API) GET(path string, handle APIHandle, options *HandleOptions)

GET register a new HTTP GET request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]

		return map[string]string{
			"username": username,
		}, nil
	}
	server.API.GET("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (API) HEAD

func (a API) HEAD(path string, handle APIHandle, options *HandleOptions)

HEAD register a new HTTP HEAD request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		return nil, nil
	}
	server.API.HEAD("/users/user/", handle, &w3.HandleOptions{})

	server.Start()
}

func (API) OPTIONS

func (a API) OPTIONS(path string, handle APIHandle, options *HandleOptions)

OPTIONS register a new HTTP OPTIONS request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		return nil, nil
	}
	server.API.OPTIONS("/users/user/", handle, &w3.HandleOptions{})

	server.Start()
}

func (API) PATCH

func (a API) PATCH(path string, handle APIHandle, options *HandleOptions)

PATCH register a new HTTP PATCH request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	type userRequestType struct {
		FirstName string `json:"first_name"`
	}

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]
		params := userRequestType{}
		if err := request.DecodeJSON(&params); err != nil {
			return nil, err
		}

		return map[string]string{
			"first_name": params.FirstName,
			"username":   username,
		}, nil
	}
	server.API.PATCH("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (API) POST

func (a API) POST(path string, handle APIHandle, options *HandleOptions)

POST register a new HTTP POST request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	type userRequestType struct {
		FirstName string `json:"first_name"`
	}

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]
		params := userRequestType{}
		if err := request.DecodeJSON(&params); err != nil {
			return nil, err
		}

		return map[string]string{
			"first_name": params.FirstName,
			"username":   username,
		}, nil
	}
	server.API.POST("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (API) PUT

func (a API) PUT(path string, handle APIHandle, options *HandleOptions)

PUT register a new HTTP PUT request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	type userRequestType struct {
		FirstName string `json:"first_name"`
	}

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]
		params := userRequestType{}
		if err := request.DecodeJSON(&params); err != nil {
			return nil, err
		}

		return map[string]string{
			"first_name": params.FirstName,
			"username":   username,
		}, nil
	}
	server.API.PUT("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

type APIHandle

type APIHandle func(request *Request, response *APIResponse) (any, *Error)

APIHandle describes a method signature for handling an API request

type APIResponse

type APIResponse struct {
	// Additional headers to append to the response.
	Headers http.Header
	// Cookies to set on the response.
	Cookies []http.Cookie
}

APIResponse describes additional response properties for API handles

type Decoder

type Decoder interface {
	Decode(v any) error
}

Decoder describes a generic interface that has a Decode function

type Error

type Error struct {
	Code    int    `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
}

Error describes an API error object

func ValidationError

func ValidationError(format string, v ...any) *Error

ValidationError convenience method to make an error object for validation errors

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]

		return nil, w3.ValidationError("No user with username %s", username)
	}
	server.API.GET("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

type HTTP

type HTTP struct {
	// contains filtered or unexported fields
}

HTTP describes an HTTP server. HTTP handles are exposed to the raw http request and response writers.

func (HTTP) DELETE

func (h HTTP) DELETE(path string, handle HTTPHandle, options *HandleOptions)

DELETE register a new HTTP DELETE request handle

Example
package main

import (
	"net/http"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		username := r.Parameters["username"]

		w.Header().Set("X-Username", username)
		w.WriteHeader(200)
	}
	server.HTTP.DELETE("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTP) GET

func (h HTTP) GET(path string, handle HTTPHandle, options *HandleOptions)

GET register a new HTTP GET request handle

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		f, _ := os.Open("/foo/bar")
		info, _ := f.Stat()
		w.Header().Set("Content-Type", "text/plain")
		w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
		io.Copy(w, f)
	}
	server.HTTP.GET("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTP) HEAD

func (h HTTP) HEAD(path string, handle HTTPHandle, options *HandleOptions)

HEAD register a new HTTP HEAD request handle

Example
package main

import (
	"net/http"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		w.Header().Set("X-Fancy-Header", "Some value")
		w.WriteHeader(204)
	}
	server.HTTP.HEAD("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTP) OPTIONS

func (h HTTP) OPTIONS(path string, handle HTTPHandle, options *HandleOptions)

OPTIONS register a new HTTP OPTIONS request handle

Example
package main

import (
	"net/http"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		w.Header().Set("X-Fancy-Header", "Some value")
		w.WriteHeader(200)
	}
	server.HTTP.OPTIONS("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTP) PATCH

func (h HTTP) PATCH(path string, handle HTTPHandle, options *HandleOptions)

PATCH register a new HTTP PATCH request handle

Example
package main

import (
	"net/http"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		username := r.Parameters["username"]

		w.Header().Set("X-Username", username)
		w.WriteHeader(200)
	}
	server.HTTP.PATCH("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTP) POST

func (h HTTP) POST(path string, handle HTTPHandle, options *HandleOptions)

POST register a new HTTP POST request handle

Example
package main

import (
	"net/http"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		username := r.Parameters["username"]

		w.Header().Set("X-Username", username)
		w.WriteHeader(200)
	}
	server.HTTP.POST("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTP) PUT

func (h HTTP) PUT(path string, handle HTTPHandle, options *HandleOptions)

PUT register a new HTTP PUT request handle

Example
package main

import (
	"net/http"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(w http.ResponseWriter, r *w3.Request) {
		username := r.Parameters["username"]

		w.Header().Set("X-Username", username)
		w.WriteHeader(200)
	}
	server.HTTP.PUT("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

type HTTPEasy

type HTTPEasy struct {
	// contains filtered or unexported fields
}

HTTPEasy describes a simple to use HTTP router. HTTPEasy handles are expected to return a reader and specify the content type and length themselves.

HTTP abstracts many features away from the caller, providing a simpler experience when a only a simple HTTP server is needed. If you require more control, use the HTTP router.

The HTTPEasy server supports HTTP range requests, should the client request it and the application provide a supported Reader io.ReadSeekCloser.

func (HTTPEasy) DELETE

func (h HTTPEasy) DELETE(path string, handle HTTPEasyHandle, options *HandleOptions)

DELETE register a new HTTP DELETE request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		username := request.Parameters["username"]
		return &w3.HTTPResponse{
			Headers: map[string]string{
				"X-Username": username,
			},
		}
	}
	server.HTTPEasy.DELETE("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) GET

func (h HTTPEasy) GET(path string, handle HTTPEasyHandle, options *HandleOptions)

GET register a new HTTP GET request handle

Example
package main

import (
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		f, err := os.Open("/foo/bar")
		info, ierr := f.Stat()
		if err != nil || ierr != nil {
			return &w3.HTTPResponse{
				Status: 500,
			}
		}
		return &w3.HTTPResponse{
			Reader:        f, // The file will be closed automatically
			ContentType:   "text/plain",
			ContentLength: uint64(info.Size()),
		}
	}
	server.HTTPEasy.GET("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) GETHEAD

func (h HTTPEasy) GETHEAD(path string, handle HTTPEasyHandle, options *HandleOptions)

GETHEAD registers both an HTTP GET and HTTP HEAD request handle. Equal to calling HTTPEasy.GET and HTTPEasy.HEAD.

Handle responses can always return a reader, it will automatically be ignored for HEAD requests.

Example
package main

import (
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		f, err := os.Open("/foo/bar")
		info, ierr := f.Stat()
		if err != nil || ierr != nil {
			return &w3.HTTPResponse{
				Status: 500,
			}
		}
		return &w3.HTTPResponse{
			Reader:        f, // the file will not be read for HTTP HEAD requests, but it will be closed.
			ContentType:   "text/plain",
			ContentLength: uint64(info.Size()),
		}
	}
	server.HTTPEasy.GETHEAD("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) HEAD

func (h HTTPEasy) HEAD(path string, handle HTTPEasyHandle, options *HandleOptions)

HEAD register a new HTTP HEAD request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		return &w3.HTTPResponse{
			Headers: map[string]string{
				"X-Fancy-Header": "some value",
			},
		}
	}
	server.HTTPEasy.HEAD("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) OPTIONS

func (h HTTPEasy) OPTIONS(path string, handle HTTPEasyHandle, options *HandleOptions)

OPTIONS register a new HTTP OPTIONS request handle

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		return &w3.HTTPResponse{
			Headers: map[string]string{
				"X-Fancy-Header": "some value",
			},
		}
	}
	server.HTTPEasy.OPTIONS("/users/user", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) PATCH

func (h HTTPEasy) PATCH(path string, handle HTTPEasyHandle, options *HandleOptions)

PATCH register a new HTTP PATCH request handle

Example
package main

import (
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		username := request.Parameters["username"]

		f, err := os.Open("/foo/bar")
		if err != nil {
			return &w3.HTTPResponse{
				Status: 500,
			}
		}
		return &w3.HTTPResponse{
			Headers: map[string]string{
				"X-Username": username,
			},
			Reader: f,
		}
	}
	server.HTTPEasy.PATCH("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) POST

func (h HTTPEasy) POST(path string, handle HTTPEasyHandle, options *HandleOptions)

POST register a new HTTP POST request handle

Example
package main

import (
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		username := request.Parameters["username"]

		f, err := os.Open("/foo/bar")
		if err != nil {
			return &w3.HTTPResponse{
				Status: 500,
			}
		}
		return &w3.HTTPResponse{
			Headers: map[string]string{
				"X-Username": username,
			},
			Reader: f,
		}
	}
	server.HTTPEasy.POST("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) PUT

func (h HTTPEasy) PUT(path string, handle HTTPEasyHandle, options *HandleOptions)

PUT register a new HTTP PUT request handle

Example
package main

import (
	"os"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request) *w3.HTTPResponse {
		username := request.Parameters["username"]

		f, err := os.Open("/foo/bar")
		if err != nil {
			return &w3.HTTPResponse{
				Status: 500,
			}
		}
		return &w3.HTTPResponse{
			Headers: map[string]string{
				"X-Username": username,
			},
			Reader: f,
		}
	}
	server.HTTPEasy.PUT("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (HTTPEasy) Static

func (h HTTPEasy) Static(path string, directory string, options *StaticOptions)

Static registers a GET and HEAD handle for all requests under path to serve any files matching the directory.

For example:

directory = /usr/share/www/
path      = /static/

Request for '/static/image.jpg' would read file '/usr/share/www/image.jpg'

Will panic if any handle is registered under path. Attempting to register a new handle under path after calling Static will panic.

Caching will be enabled by default for all files served by this router. The mtime of the file will be used for the Last-Modified date.

By default, the server will use the file extension (if any) to determine the MIME type for the response.

Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	server.HTTPEasy.Static("/static/*", "/path/to/static/files", nil)

	server.Start()
}

func (HTTPEasy) StaticFS

func (h HTTPEasy) StaticFS(path string, filesystem fs.FS, fsRoot string, options *StaticOptions)

StaticFS registers a GET and HEAD handle for all requests under path to serve any files reading from the given filesystem.

For example:

directory = /resources/
fsRoot      = /static/

Request for '/static/image.jpg' would read file '/resources/image.jpg'

Will panic if any handle is registered under path. Attempting to register a new handle under path after calling Static will panic.

Caching will be enabled by default for all files served by this router. The mtime of the file will be used for the Last-Modified date.

By default, the server will use the file extension (if any) to determine the MIME type for the response.

type HTTPEasyHandle

type HTTPEasyHandle func(request *Request) *HTTPResponse

HTTPEasyHandle describes a method signature for handling an HTTP request

type HTTPHandle

type HTTPHandle func(w http.ResponseWriter, r *Request)

HTTPHandle describes a method signature for handling an HTTP request

type HTTPResponse

type HTTPResponse struct {
	// The reader for the response. Will be closed when the HTTP response is finished. Can be nil.
	//
	// If a io.ReadSeekCloser is provided then ranged data may be provided for an HTTP range request.
	Reader io.ReadCloser
	// The status code for the response. If 0 then 200 is implied.
	Status int
	// Additional headers to append to the response.
	Headers map[string]string
	// Cookies to set on the response.
	Cookies []http.Cookie
	// The content type of the response. Will overwrite any 'content-type' header in Headers.
	ContentType string
	// The length of the content. Will overwrite any 'content-length' header in Headers.
	ContentLength uint64
}

HTTPResponse describes an HTTP response

type HandleOptions

type HandleOptions struct {
	// AuthenticateMethod method called to determine if a request is properly authenticated or not. If a method is
	// provided, then it is called for each incoming request. The value returned by this method is passed as the
	// UserData field of a [w3.Request]. Returning nil signals an unauthenticated request, which will be handled by
	// the UnauthorizedMethod (if provided) or a default handle. If the AuthenticateMethod is not provided, then the
	// UserData field is nil.
	AuthenticateMethod func(request *http.Request) any
	// PreHandle is an optional method that is called immediately upon receiving the HTTP request, before authentication
	// and before rate limit checks. This method allows servers to provide early handling of a request before any
	// processing happens.
	//
	// The returned error is only used as a nil check, the value of any error isn't used. If an error is returned then
	// no more processing is performed. It is assumed that a response will have been written to w.
	//
	// If nil is returned then the request will continue normally, no status should have been written to w. Any headers
	// added may be overwritten by the handle.
	PreHandle func(w http.ResponseWriter, request *http.Request) error
	// UnauthorizedMethod method called when an unauthenticated request occurs, i.e.AuthenticateMethod returned nil,
	// which allows you to customize the response seen by the user.
	// If omitted, a default handle is used.
	UnauthorizedMethod func(w http.ResponseWriter, request *http.Request)
	// MaxBodyLength defines the maximum length accepted for any HTTP request body. Requests that exceed this limit will
	// receive a "413 Payload Too Large" response. The default value of 0 will not reject requests with large bodies.
	MaxBodyLength uint64
	// DontLogRequests if true then requests to this handle are not logged
	DontLogRequests bool
}

HandleOptions describes options for a route

type JSONResponse

type JSONResponse struct {
	// The actual data of the response
	Data any `json:"data,omitempty"`
	// If an error occured, details about the error
	Error *Error `json:"error,omitempty"`
}

JSONResponse describes an API response object

type MockRequestParameters

type MockRequestParameters struct {
	// User data to be passed into the handler. May be nil.
	UserData any
	// URL parameters (not query parameters) to be populated into the request.Params object in the handler. May be nil.
	Parameters map[string]string
	// Object to be encoded with JSON as the body. May be nil. Exclusive to Body.
	JSONBody any
	// Body data. May be nil. Exclusive to JSONBody.
	Body io.ReadCloser
	// Optional HTTP request to pass to the handler.
	Request *http.Request
}

Parameters for creating a mock request for uses in tests

type MockResponseWriter added in v1.1.0

type MockResponseWriter struct {
	Body    *bytes.Buffer
	Headers http.Header
	Status  int
}

MockResponseWriter is an implementation of the http.ResponseWriter interface meant for use in unit tests. It also implements http.Flusher.

func NewMockResponseWriter added in v1.1.0

func NewMockResponseWriter() *MockResponseWriter

NewMockResponseWriter creates a new response writer suitable for use in unit tests.

func (*MockResponseWriter) Flush added in v1.1.0

func (rw *MockResponseWriter) Flush()

func (*MockResponseWriter) Header added in v1.1.0

func (rw *MockResponseWriter) Header() http.Header

func (*MockResponseWriter) Write added in v1.1.0

func (rw *MockResponseWriter) Write(b []byte) (int, error)

func (*MockResponseWriter) WriteHeader added in v1.1.0

func (rw *MockResponseWriter) WriteHeader(statusCode int)

type Request

type Request struct {
	*http.Request
	// URL path parameters (not query parameters). Keys do not include the ':' or '*'.
	Parameters map[string]string
	// User data provided from the result of the AuthenticateRequest method on the handle options
	UserData any
}

Request describes an API request

func MockRequest

func MockRequest(parameters MockRequestParameters) *Request

MockRequest will generate a mock request for testing your handlers. Will panic for invalid parameters.

func (Request) Decode

func (r Request) Decode(v any, decoder Decoder) *Error

Decode will unmarshal the request body to v using the given decoder

func (Request) DecodeJSON

func (r Request) DecodeJSON(v any) *Error

DecodeJSON unmarshal the JSON body to the provided interface.

Equal to calling:

r.Decode(v, json.NewDecoder(r.HTTP.Body))
Example
package main

import (
	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	type userRequestType struct {
		FirstName string `json:"first_name"`
	}

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		username := request.Parameters["username"]
		params := userRequestType{}
		if err := request.DecodeJSON(&params); err != nil {
			return nil, err
		}

		return map[string]string{
			"first_name": params.FirstName,
			"username":   username,
		}, nil
	}
	server.API.POST("/users/user/:username", handle, &w3.HandleOptions{})

	server.Start()
}

func (Request) RealRemoteAddr

func (r Request) RealRemoteAddr() net.IP

RealRemoteAddr will try to get the real IP address of the incoming connection taking proxies into consideration. This function looks for the `X-Real-IP`, `X-Forwarded-For`, and `CF-Connecting-IP` headers, and if those don't exist will return the remote address of the connection.

Will never return nil, if it is unable to get a valid address it will return 0.0.0.0

Example
package main

import (
	"fmt"

	"git.ecn.io/ian/w3"
	"github.com/ecnepsnai/logtic"
)

func main() {
	server := w3.New("127.0.0.1:8080", logtic.Log.Connect("HTTP"))

	handle := func(request *w3.Request, response *w3.APIResponse) (any, *w3.Error) {
		clientAddr := request.RealRemoteAddr().String()
		fmt.Printf("%s\n", clientAddr)
		return clientAddr, nil
	}
	server.API.POST("/ip/my_ip", handle, &w3.HandleOptions{})

	server.Start()
}

type Server

type Server struct {
	// The socket address that the server is listening on. Only populated if the server was created with w3.New().
	BindAddress *string
	// The port that this server is listening on. Only populated if the server was created with w3.New().
	ListenPort uint16
	// The JSON API server. API handles return data or an error, and all responses are wrapped in a common
	// response object; [w3.JSONResponse].
	API API
	// HTTPEasy describes a easy HTTP server. HTTPEasy handles are expected to return a reader and specify the content
	// type and length themselves.
	//
	// The HTTPEasy server supports HTTP range requests, should the client request it and the application provide a
	// supported Reader [io.ReadSeekCloser].
	HTTPEasy HTTPEasy
	// The HTTP server. HTTP handles are exposed to the raw http request and response writers.
	HTTP HTTP
	// Additional options for the server
	Options ServerOptions
	// contains filtered or unexported fields
}

Server describes an web server

func New

func New(bindAddress string, log *logtic.Source) *Server

New create a new server object that will bind to the provided address. Does not accept incoming connections until the server is started. Bind address must be in the format of "address:port", such as "localhost:8080" or "0.0.0.0:8080".

func NewListener

func NewListener(listener net.Listener, log *logtic.Source) *Server

NewListener creates a new server object that will use the given listener. Does not accept incoming connections until the server is started.

func (*Server) SetMethodNotAllowedHandle added in v1.1.1

func (s *Server) SetMethodNotAllowedHandle(h func(w http.ResponseWriter, r *http.Request))

SetMethodNotAllowedHandle will set the handle called when a request that did match a router but with the incorrect method occurs. Defaults to a plain HTTP 405 with "Method not allowed" as the body.

func (*Server) SetNotFoundHandle added in v1.1.1

func (s *Server) SetNotFoundHandle(h func(w http.ResponseWriter, r *http.Request))

SetNotFoundHandle will set the handle called when a request that does not match a registered path occurs. Defaults to a plain HTTP 404 with "Not found" as the body.

func (*Server) SetPostRequestHandle added in v1.1.1

func (s *Server) SetPostRequestHandle(h func(r *router.RequestResult))

SetPostRequestHandle will set the handle called after every request is processed, no matter if the request was successful or not.

func (*Server) SetRateLimitedHandle added in v1.1.1

func (s *Server) SetRateLimitedHandle(h func(w http.ResponseWriter, r *http.Request))

SetRateLimitedHandle will set the handle called when a request exceed the configured maximum per second limit. Defaults to a plain HTTP 429 with "Too many requests" as the body.

func (*Server) Start

func (s *Server) Start() error

Start will start the web server and listen on the socket address. This method blocks. If a server is stopped using the Stop() method, this returns no error.

func (*Server) Stop

func (s *Server) Stop()

Stop will stop the server. The Start() method will return without an error after stopping.

type ServerOptions

type ServerOptions struct {
	// Specify the maximum number of requests any given client IP address can make per second. Requests that are rate
	// limited will call the RateLimitedHandler, which you can override to customize the response.
	// Setting this to 0 disables rate limiting.
	MaxRequestsPerSecond int
	// The level to use when logging out HTTP requests. Maps to github.com/ecnepsnai/logtic levels. Defaults to Debug.
	RequestLogLevel logtic.LogLevel
	// If true then the server will not try to reply with chunked data for an HTTP range request
	IgnoreHTTPRangeRequests bool
}

type StaticOptions added in v1.0.1

type StaticOptions struct {
	router.StaticOptions
}

Additional options for Static and StaticFS handles.

Directories

Path Synopsis
Package router provides a simple & efficient parameterized HTTP router.
Package router provides a simple & efficient parameterized HTTP router.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL