sesh

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 15 Imported by: 0

README

Sesh

Go Reference

A minimal, type-safe, pluggable session manager for Go. A viable alternative to gorilla/sessions.

Features

  • Type-safe, minimal API using Go 1.18+ Generics
  • Easy-to-use middleware design
  • Pluggable session storage
  • Doesn't break websockets and server-sent events (SSE)

Install

go get github.com/matthewmueller/sesh

Example

type User struct {
  ID   int    `json:"id"`
  Name string `json:"name"`
}
type Data struct {
  User *User
}

// Initialize the session manager
sessions := sesh.New[Data]()

// Setup the router
router := http.NewServeMux()

// Login a user
router.HandleFunc("POST /sessions", func(w http.ResponseWriter, r *http.Request) {
  // Get the session from context
  session := sessions.FromRequest(r)

  // Assumes we've loaded and authenticated the user
  session.User = &User{
    ID:   1,
    Name: "Alice",
  }

  http.Redirect(w, r, "/", http.StatusFound)
})

// Show the user if they're logged in
router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
  // Get the session from context
  session := sessions.FromRequest(r)

  // Logged in state
  if session.User != nil {
    w.Write([]byte("Welcome " + session.User.Name))
    return
  }

  // Logged out state
  w.Write([]byte("Welcome!"))
})

// Automatically find and update the session on each request
handler := sessions.Middleware(router)

// Listen on :8080
http.ListenAndServe(":8080", handler)

Making authenticated requests outside of the Browser

For HTTP calls to an app using sesh.Middleware, create a client with the session data the request should have. This is useful for calling authenticated webhook handlers, internal endpoints and impersonating users:

client, err := sessions.Client(Data{
  User: &User{
    ID: 1,
    Name: "Alice",
  },
})
if err != nil {
  return err
}

resp, err := client.Post(webhookURL, "application/json", strings.NewReader(`{"event":"updated"}`))
if err != nil {
  return err
}
defer resp.Body.Close()

For a custom http.Client, you can use sessions.Transport(data) to get the underlying http.RoundTripper.

Flash messages

Use sessions.Flash inside session middleware to queue string values for the next request:

sessions.Flash.Set(r, "notice", "Board created")
sessions.Flash.Add(r, "error", "Name is required")
http.Redirect(w, r, "/boards", http.StatusSeeOther)

On the next request, sessions.Flash.Get(r, "notice") returns the first value. sessions.Flash.All(r) returns a deep copy of all current flashes; use flashes.Values("error") to read multiple values. Reads do not consume flashes. Current flashes expire when the request completes, even if they were never read.

Set and Add queue values for the next request. Del(r, key) and Clear(r) remove pending values without changing the current request's flashes. Application session data needs no flash fields, and middleware handles persistence.

Sessions are stored as JSON, wrapping application data and flashes. Sessions that cannot be decoded are reset. Custom codecs must encode the same value to the same bytes, since sessions are only saved when their encoding changes.

Streaming responses

Responses aren't buffered, so handlers can stream. The session is saved right before the headers are written, so make changes that need a cookie, such as logging in, before writing the body:

session := sessions.FromRequest(r)
session.User = user // change the session first, so the cookie is sent
w.Write([]byte("Welcome " + user.Name))

Changes after the first write are still saved for existing sessions, once the handler returns. A new session can't set its cookie after the headers are sent, so late changes to it are reported to ErrorHandler instead.

Session Storage Plugins

  • memory: Default in-memory store. These sessions will last until your server is restart.
  • pgxstore: PostgreSQL store using pgx.
  • sqstore: SQLite store. Bring your own *sql.DB.
  • mockstore: Mockable store. Primarily used for testing.

Missing a Store? Open a PR!

FAQ

How does this compare to gorilla/sessions?

This library is newer, so it has a more modern API. It's also much less battle-tested. Gorilla has many more session stores.

In gorilla, you'll typically get and set sessions like this:

func Handler(w http.ResponseWriter, r *http.Request) {
  // Get a session. We're ignoring the error resulted from decoding an
  // existing session: Get() always returns a session, even if empty.
  session, _ := store.Get(r, "session-name")
  // Set some session values.
  session.Values["foo"] = "bar"
  session.Values[42] = 43
  // Save it before we write to the response/return from the handler.
  err := session.Save(r, w)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
}

Here's what that looks like in sesh. You'll notice a bit less verbosity, along with not needing to save the session at the end:

type Data struct {
  Foo string
  FortyTwo int
}

var sessions = sesh.New[Data]()

func Handler(w http.ResponseWriter, r *http.Request) {
  session := sessions.Session(r)
  // Set some session values.
  session.Foo = "bar"
  session.FortyTwo = 42
How does this library compare to alexedwards/scs?

Sesh was a successful experiment in trying get a type-safe session store. I also wanted a smaller API surface. As with gorilla/sessions, scs is much more battle-tested and has a larger set of session stores.

The libraries share a similar API:

func main() {
	// Initialize a new session manager
	sessionManager = scs.New()

	mux := http.NewServeMux()
	mux.HandleFunc("/put", putHandler)
	mux.HandleFunc("/get", getHandler)

	// Setup the session middleware
	http.ListenAndServe(":4000", sessionManager.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	// Store a new key and value in the session data.
	sessionManager.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	// Use the GetString helper to retrieve the string value associated with a
	// key. The zero value is returned if the key does not exist.
	msg := sessionManager.GetString(r.Context(), "message")
	io.WriteString(w, msg)
}

While sesh would look like:

type Data struct {
  Message string
}

func main() {
	// Initialize a new session manager
	sessionManager = sesh[Data].New()

	mux := http.NewServeMux()
	mux.HandleFunc("/put", putHandler)
	mux.HandleFunc("/get", getHandler)

  // Setup the session middleware
	http.ListenAndServe(":4000", sessionManager.Middleware(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	// Store a new key and value in the session data.
  session := sessionManager.Session(r)
  session.Message = "Hello from a session!"
}

func getHandler(w http.ResponseWriter, r *http.Request) {
  session := sessionManager.Session(r)
	msg := session.Message
	io.WriteString(w, msg)
}

Similar to scs, there is no store for saving the session within a cookie. A stateless session needs a different Store interface because it's tied to HTTP and needs to live within the request-response lifecycle.

I thought I'd miss this capability, but it turns out you get a lot of nice features if you store your sessions externally. You get the ability to:

  • Tie much more data to a session
  • Clear all sessions (e.g. log everyone out)
  • Manipulate sessions outside of HTTP (e.g. workers)

Thanks

  • Alex Edwards (@alexedwards) for creating scs, which was a big inspiration for this project.

Contributions

We welcome all contributions! Pull requests, bug reports and features requests are all appreciated.

If you have an idea or are unsure how to contribute, open an issue!

Contributors

License

MIT

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Codec

type Codec interface {
	Encode(v any) ([]byte, error)
	Decode(data []byte, v any) error
}

Codec encodes session payloads for the store. Encoding the same value must produce the same bytes, so unchanged sessions aren't saved again.

type Cookie struct {
	// Name sets the name of the session cookie. It should not contain
	// whitespace, commas, colons, semicolons, backslashes, the equals sign or
	// control characters as per RFC6265. The default cookie name is "session".
	// If your application uses two different sessions, you must make sure that
	// the cookie name for each is unique.
	Name string

	// Domain sets the 'Domain' attribute on the session cookie. By default
	// it will be set to the domain name that the cookie was issued from.
	Domain string

	// HttpOnly sets the 'HttpOnly' attribute on the session cookie. The
	// default value is true.
	HttpOnly bool

	// Path sets the 'Path' attribute on the session cookie. The default value
	// is "/". Passing the empty string "" will result in it being set to the
	// path that the cookie was issued from.
	Path string

	// Persist sets whether the session cookie should be persistent or not
	// (i.e. whether it should be retained after a user closes their browser).
	// The default value is true, which means that the session cookie will not
	// be destroyed when the user closes their browser and the appropriate
	// 'Expires' and 'MaxAge' values will be added to the session cookie. If you
	// want to only persist some sessions (rather than all of them), then set this
	// to false and call the RememberMe() method for the specific sessions that you
	// want to persist.
	ExpireIn time.Duration

	// SameSite controls the value of the 'SameSite' attribute on the session
	// cookie. By default this is set to 'SameSite=Lax'. If you want no SameSite
	// attribute or value in the session cookie then you should set this to 0.
	SameSite http.SameSite

	// Secure sets the 'Secure' attribute on the session cookie. The default
	// value is false. It's recommended that you set this to true and serve all
	// requests over HTTPS in production environments.
	// See https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md#transport-layer-security.
	Secure bool
}

Cookie contains the configuration settings for session cookies.

type Flashes added in v0.1.1

type Flashes map[string][]string

Flashes holds string values by case-sensitive key. Initialize it before calling Set or Add, for example with make(Flashes).

func (Flashes) Add added in v0.1.1

func (f Flashes) Add(key, value string)

Add appends value to the values for key.

func (Flashes) Clear added in v0.1.1

func (f Flashes) Clear()

Clear removes all keys and values.

func (Flashes) Del added in v0.1.1

func (f Flashes) Del(key string)

Del removes key and its values.

func (Flashes) Get added in v0.1.1

func (f Flashes) Get(key string) string

Get returns the first value for key, or an empty string if none exists.

func (Flashes) Set added in v0.1.1

func (f Flashes) Set(key, value string)

Set replaces the values for key with value.

func (Flashes) Values added in v0.1.1

func (f Flashes) Values(key string) []string

Values returns the values for key.

type Manager

type Manager[Data any] struct {
	Cookie *Cookie
	Store  Store
	Codec  Codec
	Flash  *flashManager[Data]

	// ErrorHandler is called when an error occurs in the middleware. Once the
	// response has started, what it writes is discarded, so it should also log.
	// Default is to log and return a 500 status code with the error message.
	ErrorHandler func(http.ResponseWriter, *http.Request, error)

	// Now is used to get the current time. This is useful for testing.
	Now func() time.Time

	// Generate is used to generate a new session id.
	Generate func() (string, error)
}

Manager manages sessions

func New

func New[Data any]() *Manager[Data]

New session manager

func (*Manager[Data]) Client added in v0.1.2

func (m *Manager[Data]) Client(data Data) (*http.Client, error)

func (*Manager[Data]) Delete

func (m *Manager[Data]) Delete(ctx context.Context, id string) (err error)

Delete the session from the store

func (*Manager[Data]) From added in v0.1.0

func (m *Manager[Data]) From(ctx context.Context) (session *Data)

From returns the session data from the context

func (*Manager[Data]) FromRequest added in v0.1.0

func (m *Manager[Data]) FromRequest(r Request) (session *Data)

FromRequest returns the session data from the request

func (*Manager[Data]) Load

func (m *Manager[Data]) Load(ctx context.Context, id string) (*Session[*Data], error)

Load the session from the store

func (*Manager[Data]) Middleware

func (m *Manager[Data]) Middleware(next http.Handler) http.Handler

Middleware loads the session and saves it when it changes. The response is not buffered, so the session is saved right before the headers are written. Make changes that need a cookie, such as logging in, before writing the body. Later changes to an existing session are saved when the handler returns. Later changes to a new session can't set a cookie, so they're reported to ErrorHandler instead.

func (*Manager[Data]) Read

func (m *Manager[Data]) Read(r Request) (session *Session[*Data], err error)

Read the session from the request

func (*Manager[Data]) Save

func (m *Manager[Data]) Save(ctx context.Context, session *Session[*Data]) (err error)

Save the session to the store

func (*Manager[Data]) Transport added in v0.1.2

func (m *Manager[Data]) Transport(data Data) (http.RoundTripper, error)

Transport sends requests with a session initialized with data.

func (*Manager[Data]) Write

func (m *Manager[Data]) Write(w ResponseWriter, r Request, session *Session[*Data]) (err error)

Write the session to the response

type Request

type Request interface {
	Context() context.Context
	Cookie(name string) (*http.Cookie, error)
}

Request is the minimal interface required for loading cookies

type ResponseWriter

type ResponseWriter interface {
	Header() http.Header
}

ResponseWriter is the minimal interface required for setting cookies

type Session

type Session[Data any] struct {
	ID     string // Will be empty if the session is new
	Data   Data
	Expiry time.Time
	// contains filtered or unexported fields
}
Example
package main

import (
	"net/http"

	"github.com/matthewmueller/sesh"
)

func main() {
	type User struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}
	type Data struct {
		User *User
	}
	sessions := sesh.New[Data]()
	router := http.NewServeMux()

	// Login a user
	router.HandleFunc("POST /sessions", func(w http.ResponseWriter, r *http.Request) {
		session := sessions.FromRequest(r)
		// Assumes we've loaded and authenticated the user
		session.User = &User{
			ID:   1,
			Name: "Alice",
		}
		http.Redirect(w, r, "/", http.StatusFound)
	})

	// Show the user if they're logged in
	router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		session := sessions.FromRequest(r)
		if session.User != nil {
			w.Write([]byte("Welcome " + session.User.Name))
			return
		}
		w.Write([]byte("Welcome!"))
	})

	handler := sessions.Middleware(router)
	http.ListenAndServe(":8080", handler)
}

type Store

type Store interface {
	// Find should return the data for a session id from the store. If the
	// session id is not found, expired or tampered, the data will be nil and the
	// time will be zero, but there will be no error. The err return value should
	// be used for system errors only.
	Find(ctx context.Context, id string) (data []byte, expiry time.Time, err error)

	// Upsert the session id data and expiry to the store, with the given If the
	// session id already exists, then the data and expiry time should be
	// overwritten.
	Upsert(ctx context.Context, id string, data []byte, expiry time.Time) (err error)

	// Delete removes the session id and corresponding data from the session
	// store. If the id does not exist then Delete should be a no-op and return
	// nil (not an error).
	Delete(ctx context.Context, id string) (err error)
}

Store is the interface for session stores.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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