bowtie

package module
v0.0.0-...-707360b Latest Latest
Warning

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

Go to latest
Published: Jun 28, 2015 License: BSD-3-Clause, MIT Imports: 8 Imported by: 0

README

Bowtie - Web middleware for Go

Bowtie is an HTTP middleware for Go. It makes heavy use of interfaces to provide a programming model that is both idiomatic and easily extensible (with, hopefully, relatively minimal overhead).

Here's a bit of info on how Bowtie compares to other Go Web frameworks.

Getting started

Standing up a basic Bowtie server with a default router and a few useful middlewares only requires a few lines of code:

package main

import (
    "github.com/mtabini/go-bowtie"
    "github.com/mtabini/go-bowtie/middleware"
    "github.com/mtabini/go-bowtie/quick"
    "net/http"
)

func main() {
    // Create a new Bowtie server
    s := quick.New()

    s.GET("/test/:id", func(c bowtie.Context) {
        id := c.(*middleware.RouterContext).Params.ByName("id")

        c.Response().WriteString("The ID is " + id)
    })

    http.ListenAndServe(":8000", s)
}

Like all comparable systems Bowtie draws its power on a series of middlewares that perform specific tasks. To understand how this all works, let's start, then, with the most basic server—one that does, well, nothing:

package main

import (
    "github.com/mtabini/go-bowtie"
    "net/http"
)

func main() {
    s := bowtie.NewServer()

    http.ListenAndServe(":8000", s)
}

Contexts

Bowtie works by taking an HTTP request associating it with an execution context that encapsulates its primary elements (that is, a request object and a response writer). It then executes a series of zero or more middlewares against this context until either some data is written to the output or an error occurs.

The server exposes an AddContextFactory function that can be used to extend the functionality associated with the context with your own structs. You can add multiple context factories and have each build a new context on top of the previous one.

For example, suppose that you want to store come configuration parameters inside your own context—say, the URL to your database. You can create a custom context like such:

package main

import (
    "github.com/mtabini/go-bowtie"
    "net/http"
)

type MyContext struct {
    bowtie.Context
    DBURL string
}

func MyContextFactory(previous bowtie.Context) bowtie.Context {
    // Return an instance of our context that encapsulates the previous
    // context created for the server

    return &MyContext{
        Context: previous,
        DBURL:   "db://mydb/table1",
    }
}

func main() {
    s := bowtie.NewServer()

    s.AddContextFactory(MyContextFactory)

    http.ListenAndServe(":8000", s)
}

In this fashion, you (or your middlewares) can extend the context as many times as you need in order to add more functionality to it. At runtime, you can then cast the generic instance of bowtie.Context that the server passes around to the specific object you need and access it.

Middlewares

In order to do really do anything with Bowtie, you will need to write (or, at least, use) one or more middlewares. Luckily, that's easy:

func MyMiddleware(c bowtie.Context, next func()) {
    // Cast the context to our context and get the DB URL

    myC := c.(*MyContext)

    // Output the URL to the client

    c.Response().WriteString(myC.DBURL)
}

As you can see, the middleware is simply a function that receives a context interface as its first argument. We can then cast the context to our own specialized struct and take advantage of its functionality if necessary.

The second argument to a middleware is a reference to a function that can be called to delay the execution of the middleware until after all other middlewares have run. This is handy for things like logging and error management, but you can ignore it in most cases—in fact, your middleware does not need to return anything; it can simply exit when it's done.

Middleware providers

As you can imagine, the creation of new context types and middlewares often goes hand-in-hand; therefore, a Bowtie server also defines a MiddlewareProvider interface that can be used to handily couple these two operations in a single call.

For example, we could refactor our context factory and middleware into a unified struct that gives us more reusability:

type MyMiddlewareProvider struct {
    DBURL string
}

func (m *MyMiddlewareProvider) ContextFactory() bowtie.ContextFactory {
    return func(previous bowtie.Context) bowtie.Context {
        // Return an instance of our context that encapsulates the previous
        // context created for the server

        return &MyContext{
            Context: previous,
            DBURL:   m.DBURL,
        }
    }
}

func (m *MyMiddlewareProvider) Middleware() bowtie.Middleware {
    return func(c bowtie.Context, next func()) {
        // Cast the context to our context and get the DB URL

        myC := c.(*MyContext)

        // Output the URL to the client

        c.Response().WriteString(myC.DBURL)
    }
}

func main() {
    s := bowtie.NewServer()

    s.AddMiddlewareProvider(&MyMiddlewareProvider{DBURL: "db:/my/database"})

    http.ListenAndServe(":8000", s)
}

As you can see, in addition to keeping things simple by not having to add both a middleware and a context, we also gain the ability to let the developer choose the database URL when she instantiates the provider at runtime.

Bowtie's request and response writer

Bowtie extends http.Request with a handful of functions designed to make reading the request's data a bit easier.

Also replaced is http.ResponseWriter; Bowtie comes with its own interface that provides a few additional convenience methods for writing strings and serializing JSON, as well as managing errors. In particular, you may find the functions in the form

ResponseWriter.WriteXXXOrError(data, error)

useful for dealing with the common Go pattern of returning a (data, error) tuple from a function call. If the error is not nil, it is added to the response writer; otherwise, the data is written to the output stream. For example:


func f() (string, error) {
    return "test", nil
}

func middleware (c bowtie.Context, next func()) {
    c.Response().WriteStringOrError(f())
}

Since the response writer is an interface (downwards compatible with http.ResponseWriter, of course), you are free to extend its functionality as needed. Simply create your own ResponseWriterFactory function and set your server's eponymous property to it.

Error management

In an attempt to minimize the likelihood of sensitive data leakage, Bowtie encapsulates errors inside a special Error interface that can be safely written to the output stream.

Each error has an associated status code, which is automatically written to the response writer. If the status code is greater than 499, the error is assumed to be a server-side problem, and a generic message is written to the stream instead of the actual error message. Naturally, this is also true if the error is marshalled to JSON.

New Error instances can be created by calling NewError, which also offers fmt.Printf-like templating capabilities. You can also convert an existing Go error to Error by calling NewErrorWithError; in this case, the new error instance is assigned a status code of 500.

Bowtie's response writer maintains its own error array, which is populated either by calling one of the error-aware convenience functions, or by explicitly adding new instances with a call to AddError. Errors passed into the array are automatically encapsulated by an Error-compliant struct that renders them safe for output.

Note, however, that the server doesn't actually do anything with the error array; in order to output the errors to the users, you will need to use the Error middleware (see below).

Routing

Although you are free to use your own router, Bowtie comes with a slightly modified copy of Julien Schmidt's trie-based httprouter that is ready to go:

package main

import (
    "github.com/mtabini/go-bowtie"
    "github.com/mtabini/go-bowtie/middleware"
    "net/http"
    "strconv"
)

func echoValue(c bowtie.Context) {
    id := c.(*middleware.RouterContext).Params.ByName("id")

    c.Response().WriteString("The ID is " + id)
}

func validateValue(c bowtie.Context) {
    id := c.(*middleware.RouterContext).Params.ByName("id")

    if _, err := strconv.ParseInt(id, 10, 64); err != nil {
        c.Response().AddError(bowtie.NewError(400, "Invalid, non-numeric ID %s", id))
    }
}

func ExampleServer_routing() {
    // Create a new Bowtie server
    s := bowtie.NewServer()

    // Register middlewares

    r := middleware.NewRouter()

    r.GET("/test/:id", echoValue)
    r.GET("/validate/:id", validateValue, echoValue)

    s.AddMiddleware(middleware.ErrorReporter)
    s.AddMiddlewareProvider(r)

    // bowtie.Server can be used directly with http.ListenAndServe
    http.ListenAndServe(":8000", s)
}

The main changes from the Schmidt's original router are as follows:

  • Bowtie's router defines its own context, called RouterContext; this contains a Params property that encapsulates the router's parameters.
  • Bowtie's version passes bowtie.Context to its handlers instead of instances of the HTTP request and response writer.
  • Bowtie's version supports multiple handlers per router, which are chained together and executed in sequence until either the end of the list is reached or one of the handlers writes to the output stream. The example above takes advantage of this feature by prepending the validateValue handler to echoValue and only allowing the latter to run if the data passed to by the client satisfies certain criteria.
  • Finally, Bowtie's router introduces a GetSupportedMethods function that can be used to determine which HTTP methods are supported for a given route. This, in turn, is used by the CORS middleware to respond to OPTIONS requests properly.

Bundled middlewares

Bowtie comes bundled with a few more middlewares:

  • CORSHandler makes quick work of handling CORS requests, and information from the router to provide precise answers to OPTIONS pre-flight requests.
  • ErrorReporter handles errors and outputs them safely to the output stream.
  • Logger handles logging. It comes with both plaintext and Bunyan output handlers.
  • Recovery handles panics gracefully, turning them into 500 errors and capturing all the appropriate details for later logging.

The quick server

What about the quick server listed at the beginning? It provides a simple set of defaults that gets you going very quickly; by calling New(), you get a struct that contains both a bowtie.Server and a bowtie/middleware.Router, plus a few pre-set middlewares, roughly equivalent to:

func New() *QuickServer {
    r := middleware.NewRouter()

    s := bowtie.NewServer()

    s.AddMiddleware(middleware.NewLogger(middleware.MakePlaintextLogger()))
    s.AddMiddleware(middleware.Recovery)
    s.AddMiddleware(middleware.ErrorReporter)

    cors := middleware.NewCORSHandler(r)

    cors.SetDefaults()

    s.AddMiddlewareProvider(cors)

    s.AddMiddlewareProvider(r)

    return &QuickServer{
        s,
        r,
    }
}

Bowtie compared to other Go frameworks

Bowtie borrows liberally—both in ideas and in code—from several other Go frameworks. Adopting httprouter as the default seemed like a good idea, given its raw speed. The only changes made were integrating Julien Schmidt's code with Bowtie's context-based execution, and allowing multiple handlers to be attached to a particular route.

Bowtie was also inspired by Go-Martini's simplicity and immediateness. Even though Martini's design is not idiomatic to Go, it is perhaps one of the easiest frameworks to pick up and use. In addition to adopting bits of its code, Bowtie strives for the same kind of approachable and immediateness.

Finally, the idea of a running context is borrowed from gin-gonic. The main difference between the two is that Bowtie forces the use of Go interface for carrying custom information through the context, whereas Gin allows you to append arbitrary data to it. This seemed like a more sensible approach that allows Go to do its job by providing type security and strictness.

Bowtie's error management comes from my personal fixation with safety. I don't want to leak information, and I don't want developers to worry about whether they will.

Benchmarks

None. Most of the slowness in a web app comes from places other than the basic framework used to run it, and therefore benchmarks that are only concerned with the raw speed of a router are a bit misleading.

That said, Bowtie reliance on httprouter should mean that you can expect comparable speed from it—and perhaps a level of efficiency equivalent with Gin's.

Documentation

Overview

Package bowtie provides a Web middleware for Go apps.

More information at https://github.com/mtabini/go-bowtie

For a quick start, check out the examples at http://godoc.org/github.com/mtabini/go-bowtie/quick

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Context

type Context interface {
	// Get returns a property set into the context
	Get(ContextKey) interface{}

	// Set sets a new property into the context
	Set(ContextKey, interface{})

	// Request returns the request object associated with this request
	Request() *Request

	// Response returns the response writer associated with this request
	Response() ResponseWriter

	// GetRunningTime returns the amount of time during which this request has been running
	GetRunningTime() time.Duration
}

Interface Context represents a server's context, which provides information used by the middleware. The basic context deals primarily with providing an interface to the request and response

func NewContext

func NewContext(r *http.Request, w http.ResponseWriter) Context

NewContext is a ContextFactory that creates a basic context. You will probably want to create your own context and context factory that extends the basic context for your uses

type ContextFactory

type ContextFactory func(context Context)

ContextFactory is a function that processes a context. Your application (and each middleware) can provide its own factory when the server is created, thus allowing you to set new values into the context as needed

type ContextInstance

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

Struct ContextInstance is a concrete implementation of the base server context. Your application can safely incorporate it into its own structs to extend the functionality provided by Bowtie

func (*ContextInstance) Get

func (c *ContextInstance) Get(key ContextKey) interface{}

func (*ContextInstance) GetRunningTime

func (c *ContextInstance) GetRunningTime() time.Duration

GetRunningTime returns the amount of time during which this request has been running

func (*ContextInstance) Request

func (c *ContextInstance) Request() *Request

Request returns the request associated with the context

func (*ContextInstance) Response

func (c *ContextInstance) Response() ResponseWriter

Response returns the response writer assocaited with the context

func (*ContextInstance) Set

func (c *ContextInstance) Set(key ContextKey, value interface{})

type ContextKey

type ContextKey int64

func GenerateContextKey

func GenerateContextKey() ContextKey

type Error

type Error interface {
	error
	fmt.Stringer
	json.Marshaler
	// StatusCode return the error's status code
	StatusCode() int
	// Message returns the error's message
	Message() string
	// Data returns the error's associated data
	Data() interface{}
	// SetData sets the error's associated data
	SetData(interface{})
	// PrivateRepresentation a private representation of the error. Useful for logging.
	PrivateRepresentation() map[string]interface{}
	// GetStackTrace returns the stack trace associated with this error, if any
	StackTrace() []StackFrame
	// RecordStackTrace captures a stack track and return the error instance
	CaptureStackTrace() Error
}

Interface Error represents a Bowtie error, which extends the standard error interface to provide additional Web-friendly functionality.

Instances of Error must be able to provide both a public and private representation; the latter may include sensitive information like stack traces and private messages, while the former can be safely outputted to an end user.

func NewError

func NewError(statusCode int, format string, arguments ...interface{}) Error

NewError builds a new Error instance; the `format` and `arguments` parameters work as in `fmt.Sprintf()`

func NewErrorWithError

func NewErrorWithError(err error) Error

NewErrorFromError builds a new Error instance starting from a regular Go error (or something that can be cast to it). If an instance of Error is passed to it, the function returns a copy thereof (and not the original), but _not_ of the associated data, which may be copied by reference.

If the error

type ErrorInstance

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

Struct ErrorInstance incorporates an error and associates it with an HTTP status code (assumed to be 500 if not present. Arbitrary data can also be added to the error for logging purposes, as can a stack trace.

The ErrorInstance struct is smart enough that, when asked for serialization either through error's Error() method or by JSON marshalling, it does not leak any sensitive information if the StatusCode is >= 500 (which indicates a server error).

For status codes that indicate user errors ([400-499]), the struct allows public consumers to see the actual message that was provided at initialization time.

func (*ErrorInstance) CaptureStackTrace

func (e *ErrorInstance) CaptureStackTrace() Error

func (*ErrorInstance) Data

func (e *ErrorInstance) Data() interface{}

Returns the data associated with e

func (*ErrorInstance) Error

func (e *ErrorInstance) Error() string

Satisfy the error, fmt.Stringer, and json.Marshaler interfaces

func (*ErrorInstance) MarshalJSON

func (e *ErrorInstance) MarshalJSON() ([]byte, error)

func (*ErrorInstance) Message

func (e *ErrorInstance) Message() string

Returns the message associated with e

func (*ErrorInstance) PrivateRepresentation

func (e *ErrorInstance) PrivateRepresentation() map[string]interface{}

Returns a private representation of e

func (*ErrorInstance) SetData

func (e *ErrorInstance) SetData(data interface{})

Sets the data associated with e

func (*ErrorInstance) StackTrace

func (e *ErrorInstance) StackTrace() []StackFrame

func (*ErrorInstance) StatusCode

func (e *ErrorInstance) StatusCode() int

Returns the status code associated with e

func (*ErrorInstance) String

func (e *ErrorInstance) String() string

type Middleware

type Middleware func(c Context, next func())

Middleware is a function that encapsulate a Bowtie middleware. It receives an execution context and a next function it can optionally call to delay its own execution until the end of the request (useful for logging, error handling, etc.)

type MiddlewareProvider

type MiddlewareProvider interface {
	Middleware() Middleware
	ContextFactory() ContextFactory
}

Interface MiddleProvider can be implemented by structs that want to offer both a middleware and a context factory. They can be installed onto a server by calling AddMiddlewareProvider()

type Request

type Request struct {
	*http.Request
}

Struct Request adds a few convenience functions to `http.Request`.

func NewRequest

func NewRequest(r *http.Request) *Request

NewRequest creates a new request instance. This is called transparently for you at the time the server receives a request

func (*Request) JSONBody

func (r *Request) JSONBody() (map[string]interface{}, error)

JSONBody attempts to unmarshal JSON out of the request's body, and returns a map if successful, or an error if not.

func (*Request) ReadJSONBody

func (r *Request) ReadJSONBody(v interface{}) error

ReadJSONBody attempts to unmarshal JSON from the request's body into a destination of your choosing.

func (*Request) StringBody

func (r *Request) StringBody() (string, error)

StringBody returns the request's body as a string

type ResponseWriter

type ResponseWriter interface {
	http.ResponseWriter

	// Add error safely adds a new error to the context, converting it to bowtie.Error if appropriate
	AddError(err error)

	// Errors returns an array that contains any error assigned to the response writer
	Errors() []Error

	// Status returns the HTTP status code of the writer. You can set this by using `WriteHeader()`
	Status() int

	// Written returns true if any data (including a status code) has been written to the writer's
	// output stream
	Written() bool

	// WriteOrError checks if `err` is not nil, in which case it adds it to the context's error
	// list and returns. If `err` is nil, `p` is written to the output stream instead. This is a
	// convenient way of dealing with functions that return (data, error) tuples inside a middleware
	WriteOrError(p []byte, err error) (int, error)

	// WriteString is a convenience method that outputs a string
	WriteString(s string) (int, error)

	// WriteStringOrError is a convenience method that outputs a string or adds an error to the writer.
	// It works like `WriteOrError`, but takes string instead of a byte array
	WriteStringOrError(s string, err error) (int, error)

	// WriteJSON writes data in JSON format to the output stream. The output Content-Type header
	// is also automatically set to `application/json`
	WriteJSON(data interface{}) (int, error)

	// WriteJSONOrError checks if `err` is not nil, in which case it adds it to the context's error
	// list and returns. If `err` is nil, `data` is serialized to JSON and written to the output
	// stream instead; the Content-Type of the response is also set to `application/json` automatically.
	// This is a convenient way of dealing with functions that return (data, error) tuples inside
	// a middleware
	WriteJSONOrError(data interface{}, err error) (int, error)
}

Interface ResponseWriter extends the functionality provided by `http.ResponseWriter`, mainly by adding a few convenience methods for writing strings and JSON data and dealing with errors.

You can provide your own extended ResponseWriter by creating a custom ResponseWriterFactory function and setting it to the ResponseWriterFactory property of your server.

func NewResponseWriter

func NewResponseWriter(w http.ResponseWriter) ResponseWriter

type ResponseWriterFactory

type ResponseWriterFactory func(w http.ResponseWriter) ResponseWriter

type ResponseWriterInstance

type ResponseWriterInstance struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

func (*ResponseWriterInstance) AddError

func (r *ResponseWriterInstance) AddError(err error)

Add error safely adds a new error to the context, converting it to bowtie.Error if appropriate

func (*ResponseWriterInstance) Errors

func (r *ResponseWriterInstance) Errors() []Error

Errors returns an array that contains any error assigned to the response writer

func (*ResponseWriterInstance) Status

func (r *ResponseWriterInstance) Status() int

Status returns the HTTP status code of the writer. You can set this by using `WriteHeader()`

func (*ResponseWriterInstance) Write

func (r *ResponseWriterInstance) Write(p []byte) (int, error)

Write implements io.Writer and outputs data to the HTTP stream

func (*ResponseWriterInstance) WriteHeader

func (r *ResponseWriterInstance) WriteHeader(status int)

WriteHeader writes a status header

func (*ResponseWriterInstance) WriteJSON

func (r *ResponseWriterInstance) WriteJSON(data interface{}) (int, error)

WriteJSON writes data in JSON format to the output stream. The output Content-Type header is also automatically set to `application/json`

func (*ResponseWriterInstance) WriteJSONOrError

func (r *ResponseWriterInstance) WriteJSONOrError(data interface{}, err error) (int, error)

WriteJSONOrError checks if `err` is not nil, in which case it adds it to the context's error list and returns. If `err` is nil, `data` is serialized to JSON and written to the output stream instead; the Content-Type of the response is also set to `application/json` automatically. This is a convenient way of dealing with functions that return (data, error) tuples inside a middleware

func (*ResponseWriterInstance) WriteOrError

func (r *ResponseWriterInstance) WriteOrError(p []byte, err error) (int, error)

WriteOrError checks if `err` is not nil, in which case it adds it to the context's error list and returns. If `err` is nil, `p` is written to the output stream instead. This is a convenient way of dealing with functions that return (data, error) tuples inside a middleware

func (*ResponseWriterInstance) WriteString

func (r *ResponseWriterInstance) WriteString(s string) (int, error)

WriteString is a convenience method that outputs a string

func (*ResponseWriterInstance) WriteStringOrError

func (r *ResponseWriterInstance) WriteStringOrError(s string, err error) (int, error)

WriteStringOrError is a convenience method that outputs a string or adds an error to the writer. It works like `WriteOrError`, but takes string instead of a byte array

func (*ResponseWriterInstance) Written

func (r *ResponseWriterInstance) Written() bool

Written returns true if any data (including a status code) has been written to the writer's output stream

type Server

type Server struct {
	ResponseWriterFactory ResponseWriterFactory
	// contains filtered or unexported fields
}

Struct Server is a Bowtie server. It provides a handler compatible with http.ListenAndServe that creates a context and executes any attached middleware.

Example (Middleware)
package main

import (
	"net/http"
)

// Struct MyContext extends Bowtie's context and adds more features to it
type MyContext struct {
	Context
	DBURL string
}

// Struct MyMiddlewareProvider satisfies the MiddlewareProvider interface
// and provides both a middleware and a context factory
type MyMiddlewareProvider struct {
	DBURL string
}

// ContextFactory, when added to a server, “wraps” our context around an
// existing context. At execution time, the middleware can then cast
// the context that the server passes to it to MyContext and take
// advantage of its functionality.
func (m *MyMiddlewareProvider) ContextFactory() ContextFactory {
	return func(previous Context) Context {
		// Return an instance of our context that encapsulates the previous
		// context created for the server

		return &MyContext{
			Context: previous,
			DBURL:   m.DBURL,
		}
	}
}

// A middleware is simply a function that takes a context, which it can
// use to manipulate the current HTTP request, and a next function that
// can be called to delay the middleware's execution until after all
// other middlewares have run.
func (m *MyMiddlewareProvider) Middleware() Middleware {
	return func(c Context, next func()) {
		// Cast the context to our context and get the DB URL

		myC := c.(*MyContext)

		// Output the URL to the client

		c.Response().WriteString(myC.DBURL)
	}
}

func main() {
	// Create a new Bowtie server
	s := NewServer()

	// Register our new middleware provider. This adds our context factory
	// to it, and injects our middleware into its execution queue.
	s.AddMiddlewareProvider(&MyMiddlewareProvider{DBURL: "db:/my/database"})

	// Server can be used directly with http.ListenAndServe
	http.ListenAndServe(":8000", s)
}
Output:

func NewServer

func NewServer() *Server

NewServer initializes and returns a new Server instance.

func (*Server) AddContextFactory

func (s *Server) AddContextFactory(value ContextFactory)

SetContextFactory changes the context factory used by the server. This allows you to create your own Context structs and use them inside your apps.

func (*Server) AddMiddleware

func (s *Server) AddMiddleware(f Middleware)

AddMiddleware adds a new middleware handler. Handlers are executed in the order in which they are added to the server

func (*Server) AddMiddlewareProvider

func (s *Server) AddMiddlewareProvider(p MiddlewareProvider)

AddMiddlewareProvider registers a new middleware provider

func (*Server) NewContext

func (s *Server) NewContext(r *http.Request, w http.ResponseWriter) Context

NewContext creates a new basic server context. You should not need to call this except for testing purposes. Instead, you should extend the server context with your struct and provide a context factory to the server

func (*Server) Run

func (s *Server) Run(c Context)

Run is the server's main entry point. It executes each middleware in sequence until one of them causes data to be written to the output

func (*Server) ServeHTTP

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

ServeHTTP handles requests and can be used as a handler for http.Server

type StackFrame

type StackFrame struct {
	Path   string `json:"path"`
	Line   int    `json:"line"`
	Func   string `json:"func"`
	Source string `json:"source"`
}

Struct StackFrame represents a frame of a stack trace

Directories

Path Synopsis
Package middleware contains several middlewares for bowtie.
Package middleware contains several middlewares for bowtie.
Package quick provides the easiest way to stand up a Bowtie server.
Package quick provides the easiest way to stand up a Bowtie server.

Jump to

Keyboard shortcuts

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