sesh

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 10 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 http.Flusher

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.Session(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.Session(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)

Install

go get github.com/matthewmueller/sesh

Session Storage Plugins

  • Memory: By default sesh initializes an in-memory store. These sessions will last until your server is restart.
  • SQLite 3: sqstore contains a SQLite 3 implementation for storing sessions in SQLite.
  • Mock: mockstore contains a mockable storage. This is 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
}
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 Manager

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

	// ErrorHandler is called when an error occurs in the middleware
	// Default is to 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]) Delete

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

Delete the session from the store

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 for loading and saving sessions

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]) Session

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

Session returns the session data from the request

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
}
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.Session(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.Session(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