Skip to content
rotationalioPublic

About

Middleware for server applications that use the Gin web framework

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Gimlet Go Reference

Middleware for server applications that use the Gin web framework

Usage

Add gimlet to your server application that uses the Gin web framework as follows:

go get go.rtnl.ai/gimlet

This will add the latest version of gimlet to your go.mod file. Middleware can be applied to all routes as follows:

func main() {
    router := gin.Default()
    router.Use(gimlet.Middleware())
}

Or can be applied to individual routes:

func main() {
    router := gin.Default()
    router.GET("/", gimlet.Middleware(), myHandler)
}

See the documentation about configuring and using individual middleware.

Logging

Our logging middleware uses go.rtnl.ai/x/rlog for output instead of the default HTTP logger. Rlog is a thin layer over log/slog: you still build attributes with slog.String, slog.Any, and so on, but the global default logger and level are owned by rlog.

To use the middleware, add your service name and semantic version to each log message:

router.Use(logger.Logger("myservice", "1.1.0"))

The logger handles errors using the c.Errors construct. If there is 1 error, then it is included as a single error attribute, otherwise all errors are included as an errors array. For example:

func MyHandler(c *gin.Context) {
    c.Error(errors.New("something bad happened"))
    c.JSON(http.StatusBadRequest, gin.H{"error": "could not complete request"})
}

Will ensure that "something bad happened" is logged with the errors. Note that 400 errors are logged at warn level, 500 errors are logged at error level and all others are logged at info level.

If you would like to add logs related to a specific request, use the Tracing helper. It returns an *rlog.Logger with the request ID attached so you can follow one request across log lines.

func MyHandler(c *gin.Context) {
    log := logger.Tracing(c)
    log.InfoAttrs(c.Request.Context(), "something happened during this request")
}

With extra structured fields, pass slog.Attr values (variadic) after the message, for example log.InfoAttrs(ctx, "user loaded", slog.String("user_id", id)).

IMPORTANT: When you have a request context, prefer rlog’s *Attrs methods (for example InfoAttrs, DebugAttrs, WarnAttrs, ErrorAttrs) and pass c.Request.Context() so records stay aligned with tracing. The *Attrs variants also avoid the extra work of slog’s generic key/value parsing.

Initializing rlog in your application

Gimlet’s logger middleware and logger.Tracing call rlog.Default(), so your main (or server constructor) should configure rlog once at startup. Rlog expects handlers to be built with rlog.MergeWithCustomLevels (so custom levels such as trace map cleanly to JSON) and usually rlog.WithGlobalLevel so the handler respects rlog.SetLevel.

import (
    "log/slog"
    "os"

    "go.rtnl.ai/x/rlog"
)

func initLogging(conf Config) {
    rlog.SetLevel(conf.GetLogLevel())

    opts := rlog.MergeWithCustomLevels(rlog.WithGlobalLevel(&slog.HandlerOptions{}))
    var h slog.Handler
    if conf.ConsoleLog {
        h = slog.NewTextHandler(os.Stdout, opts)
    } else {
        h = slog.NewJSONHandler(os.Stdout, opts)
    }
    rlog.SetDefault(rlog.New(slog.New(h)))
}

Use rlog.LevelDecoder in configuration structs so strings such as TRACE, FATAL, or PANIC decode correctly (for example with confire):

type Config struct {
    LogLevel   rlog.LevelDecoder `split_words:"true" default:"info" desc:"specify the verbosity of logging (trace, debug, info, warn, error, fatal, panic)"`
    ConsoleLog bool              `split_words:"true" default:"false" desc:"if true logs colorized human readable output instead of json"`
}

func (c Config) GetLogLevel() slog.Level {
    return c.LogLevel.Level()
}

In tests, use helpers in go.rtnl.ai/gimlet/logger: logger.TestSink returns a *rlog.CapturingTestHandler and configures rlog.SetDefault; assert with ResultMaps, Lines, or rlog.MustParseJSONLine. Also available: logger.Discard, logger.ResetLogger.

OpenTelemetry (Observability - o11y)

Gimlet has middleware to provide spans to the context of a gin request so that a request can be traced across processes and metrics can be computed and collected by OpenTelemetry collectors for observability and debugging.

You can quickly and easily get started with the o11y middleware as follows:

router.Use(o11y.Middleware("myservice"))

The middleware sets up a tracer and meter provider and also sets up an HTTP server specific meter to record HTTP request and response metrics by default.

The middleware ensures the tracer is set on the gin context (and the golang context) and ensures that any propagation from previous services is carred forward. At the end of the request the metrics are recorded.

Inside of a gin handler func, you can create a child span to start recording attributes or events as follows:

func UserDetail(c *gin.Context) {
    _, span := tracer.Start(
        c.Request.Context(), "userDetail",
    )
    defer span.End()

    if user, err := db.GetUser(c.Param("id")); err != nil {
        c.Error(err)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "could not retrieve user"})
        return
    }
    span.SetAttributes(attribute.Key("userID").String(user.ID))
}

NOTE: You do not have to record errors on the span unless you with to record them on the child span. Any error logged by c.Error() will be recorded on the request span and the span's status will be set to Error.

You can fully customize the behavior of the o11y middleware using Option functions passed to the o11y.Middleware function. The most notable are the Filter and GinFilter functions, which allow you to skip tracing of specific requests. This is particularly useful for kubernetes probes or status endpoints. For example:

func FilterProbes(r *http.Request) bool {
    switch r.URL.Path {
    case "/readyz", "/livez", "healthz":
        return false
    default:
        return true
    }
}

func FilterStatus(c *gin.Context) bool {
    if c.FullPath() == "/v1/status" {
        return false
    }
    return true
}

router.Use(
    o11y.Middleware(
        "myservice",
        o11y.WithFilter(FilterProbes),
        o11y.WithGinFilter(FilterStatus),
    ),
)

Quarterdeck & JWT Tokens

Gimlet provides middleware to authenticate with our proxy authentication service, Quarterdeck. Quarterdeck issues JWT access and refresh tokens that the gimlet middleware uses to ensure a user's claims were issued by the Quarterdeck server and to check if the claims have the permissions to perform a specific action.

Authentication

You'll need a Quarterdeck configuration URL to get started; usually something like https://auth.rotational.app/.well-known/openid-configuration -- you can then instantiate the quarterdeck middleware as follows:

const (
    quarterdeckURL = "https://auth.rotational.app/.well-known/openid-configuration"
    audience = "https://myapp.com"
)

qd, err := quarterdeck.New(quarterdeckURL, audience)
auth, err := auth.Authenticate(qd)

You can now use the Login or Authenticate endpoints of Quarterdeck to set authentication cookies or to get an access token to put into the Authorization header as a bearer token and this middleware will validate those tokens before allowing the request to be handled further or returns a 401 not authorized error.

The authentication middleware will add the claims to the gin context for handlers to get access to. To fetch the claims use:

func MyRoute(c *gin.Context) {
    claims, err := auth.GetClaims(c)
}

The authentication source can be retrieved from downstream middleware or handlers with auth.GetAuthenticationSource(c):

source, err := auth.GetAuthenticationSource(c)
switch source {
case auth.AuthenticationSourceBearer:
	// The request was authenticated with a bearer token.
case auth.AuthenticationSourceCookie:
	// The request was authenticated with cookies.
case auth.AuthenticationSourceUnknown:
	// The request was not authenticated yet or it failed.
	// In this case, the returned error should not be nil.
}

Bearer authentication takes precedence when both credentials are present. Requests authenticated after an expired access token is refreshed with cookies are reported as cookie authentication.

Synchronization

By default, quarterdeck.New synchronizes immediately, then starts a background sync loop. The loop schedules refreshes for the JWKS cache expiry, or uses SyncInterval (1 hour by default) when the response has no expiry. MinSyncInterval (20 seconds by default) is a floor for scheduled refreshes; it does not delay the initial sync. SyncTimeout defaults to 20 seconds per request, and BackoffTimeout limits the total retry period for a sync.

Customize timing by starting from the package defaults and passing the result to WithSyncTimingConfig:

syncTiming, err := quarterdeck.NewDefaultSyncTimingConfig()
if err != nil {
    return err
}
syncTiming.MinSyncInterval = 30 * time.Second

qd, err := quarterdeck.New(quarterdeckURL, audience,
    quarterdeck.WithSyncTimingConfig(syncTiming),
)

Authorization

Authorization checks to make sure the claims have all permissions specified when setting up the middleware. For example, a rest endpoint might be set up as follows:

tasks := router.Group("/tasks", authenticate)
{
    tasks.GET("/", auth.Authorize("tasks:read"), ListTasks)
    tasks.POST("/", auth.Authorize("tasks:write"), CreateTask)
    tasks.GET("/:id", auth.Authorize("tasks:read"), TaskDetail)
    tasks.PUT("/:id", auth.Authorize("tasks:write"), UpdateTask)
    tasks.DELETE("/:id", auth.Authorize("tasks:write", "tasks:delete"), DeleteTask)
}

Note that the authentication middleware must be added before any authorize handlers are added to specific routes (to ensure claims are on the context).

If the permission specified isn't in the Permissions of the claims, then a 401 not authorized error is returned.

Claims

The auth.Authenticate middleware sets the claims on the request so that you can use the claims elsewhere in your service handler. To get the authentication claims:

func MyHandler(c *gin.Context) {
    claims, err := auth.GetClaims(c)
}

The claims will be of type *auth.Claims which have the standard rotational claims attached. If you need a generic type of claims that you will type check yourself:

func MyHandler(c *gin.Context) {
    // Use whichever key the custom claims were saved on.
    claims, exists := gimlet.Get(c, gimlet.KeyUserClaims)
}

Testing

You can use the authtest package to write tests against the Authentication middleware without mocking or bypassing the middleware.

func TestMyRoute(t *testing.T) {
    srv := authtest.New() // will automatically cleanup the server when the test is complete
    client := srv.Client()

    qd, err := quarterdeck.New(srv.ConfigURL, authtest.Audience,
        quarterdeck.WithClient(client),
        quarterdeck.WithIssuer(authtest.Issuer),
    )
    require.NoError(t, err, "could not setup quarterdeck server")

    auth, err := auth.Authenticate(qd)
    require.NoError(t, err, "could not setup authenticate middleware")

    router.GET("/", auth, MyRoute)

    // Create an access token
    claims := &auth.Claims{
        Name: "Test User",
        Email: "test@example.com",
        Permissions: ["foo:read", "foo:write"],
    }
    claims.SetSubjectID(auth.SubjectUser, ulid.Make())

    accessToken, err := srv.CreateAccessToken(claims)
    require.NoError(t, err, "could not create access token")

    // Create a new request.
    req, _ := http.NewRequest(http.MethodGet, "/", nil)
    req.Header.Set("Authorization", "Bearer "+accessToken)
}

Rate Limit

The rate limit middleware prevents abuse to a service where a DDOS or spam attack tries hundreds or thousands of requests per second. The rate limit middleware uses a token bucket approach to rate limiting: the bucket is set to a maximum of "burst" number of tokens; this is the maximum requests per second that is possible. Every second, the bucket is refreshed with a "limit" number of tokens and any request that comes into the service requires a token. If there are 0 tokens, then the request is rejected with a 429 "Too Many Requests" HTTP response.

The token bucket can become negative. E.g. if the server has a per second limit of 4 requests, and a burst of 16 requests then after a quiescent period, if the server receives 32 requests in a second, it will take 5 seconds before a request can be made again, barring any additional requests (e.g. if the client respects the 429 warning).

There are two types of rate limits:

  1. Constant: rate limits all requests with the same bucket
  2. ClientIP: rate limits requests on a per-IP address basis

The rate limit middleware can be created with a configuration:

type Config struct {
    Type     string        `default:"constant" desc:"type of rate limiter to use; either ipaddr or constant"`
    Limit    float64       `default:"4.0" desc:"number of tokens that can be added to the ratelimit token bucket per second"`
    Burst    int           `default:"32" desc:"maximum number of tokens/requests in the ratelimit token bucket"`
    CacheTTL time.Duration `split_words:"true" default:"10m" desc:"interval at which the ratelimit token bucket is cleaned up, removing old IP addresses"`
}

E.g. to create a constant rate limiter you would:

rate := ratelimit.RateLimit(&ratelimit.Config{Type: "constant", Limit: 64, Burst: 256})

Similarly, to create a per-IP rate limiter:

rate := ratelimit.RateLimit(&ratelimit.Config{
    Type: "ipaddr", Limit: 4, Burst: 32, CacheTTL: 5*time.Minute
})

You can also pass a ratelimit.Limiter directly into the middleware constructor for testing purposes or to define your own rate limiter mechanism.

CSRF Protection

Cross-Site Request Forgeries occur when an attacker attempts to trick a web application into executing the actions of a logged in user, generally by using social engineering to send a link via email or chat or via an 'evil' browser extension.

Fetch Metadata (csrf/secfetch)

Headers

Fetch Metadata headers tell the server how a browser request started: Sec-Fetch-Site describes its relationship to the destination, Sec-Fetch-Mode describes the request mode, and Sec-Fetch-Dest describes what the response is for, such as a document or image.

These are browser-controlled, restricted request headers: JavaScript cannot set or change them. This lets the middleware reject requests started by another site, even when the browser automatically includes the user's login cookies. Prefer this approach over doublecookie for new applications; the frontend does not need to manage cookies, headers, and retries for CSRF tokens.

These headers are not authentication. Tools such as curl can set them freely, so keep authentication and authorization on protected endpoints. Modern browsers send them in secure contexts, normally HTTPS or localhost; make sure proxies (such as TLS terminators) forward them unchanged.

Defaults

Both CSRF subpackages are named csrf. Import go.rtnl.ai/gimlet/csrf/secfetch and use csrf.Middleware(...).

  • GET, HEAD, and OPTIONS pass without CSRF checks. These endpoints must not change application state; use WithSafeHTTPMethods to remove any of them from the bypass list.
  • For other methods, same-origin is allowed. same-site requires an Origin matching WithExpectedOrigins; sibling subdomains are not trusted automatically. cross-site is always rejected. none is rejected unless explicitly allowed.
  • Sec-Fetch-Mode and Sec-Fetch-Dest are checked first when configured. They are neither required nor restricted by default.
  • When Sec-Fetch-Site is missing or unrecognized, a supplied Origin must match WithExpectedOrigins; otherwise, if Origin is absent, a supplied Referer must match. A mismatch rejects the request before any missing/unknown-site option or fallback. A match allows the request.
  • If neither Origin nor Referer is supplied, the corresponding missing/unknown-site option is considered.
  • WithFallback is considered after those options, only when Sec-Fetch-Site, Origin, and Referer are all absent; it is never called for an unrecognized site value.

Rejections return HTTP 403 with Gimlet's standard error response for csrf verification failed for request, and X-CSRF-Error: csrf_request_rejected (available as csrf.ErrorRequestRejected). WithNamespace changes the error header name, not the status or body.

Options

Pass options to csrf.Middleware(...). Several options below relax or bypass protection; understand their effects before enabling them. In particular, use log-only mode only while observing traffic, and keep permissive metadata options disabled on cookie-authenticated routes unless the resulting risk is acceptable.

  • WithLogOnly(true): log requests that would be rejected and let them continue. Review these warnings for legitimate flows before disabling log-only mode and enforcing rejections.
  • WithSafeHTTPMethods([]string{"GET", "HEAD", "OPTIONS"}): choose which safe methods bypass checks. Only these three methods are accepted; methods such as POST can never be exempted. Pass an empty list to check every method.
  • WithExpectedOrigins([]string{"https://app.example.com"}): trust these exact origins (scheme, host, and port) for same-site requests and for Origin/Referer checks when Sec-Fetch-Site is missing or unrecognized. Use browser-facing URLs, not Docker service names.
  • WithNamespace("application"): use X-Application-CSRF-Error instead of X-CSRF-Error.
  • WithFallback(check): provide a func(*gin.Context) bool for requests where Sec-Fetch-Site, Origin, and Referer are all absent. Configured mode/destination checks run before this callback, and WithAllowMissingMetadata(true) can allow the request before it runs. Returning true accepts the request, false rejects it.
  • WithAllowMissingMetadata(true): allow requests with no Sec-Fetch-Site when neither Origin nor Referer is supplied. A supplied but untrusted Origin/Referer is rejected first.
  • WithAllowUnknownSite(true): allow an unrecognized Sec-Fetch-Site value when neither Origin nor Referer is supplied. A supplied but untrusted Origin/Referer is rejected first. This is a compatibility escape hatch, not normally needed.
  • WithAllowSiteNone(true): allow non-safe methods with Sec-Fetch-Site: none, meaning no requesting site was identified. Enable only if your application needs this behavior.
  • WithAllowedFetchModes([]string{...}) / WithAllowedFetchDestinations([]string{...}): restrict the corresponding headers when present. For example, API fetches commonly use mode cors or same-origin and destination empty; form navigations use different values.
  • WithRequireFetchMode(true) / WithRequireFetchDestination(true): reject requests missing the corresponding header. These checks happen before WithFallback, so the fallback cannot waive them. Leave these off for clients that do not send Fetch Metadata.

The three WithAllow... settings default to false; mode/destination allowlists default to unrestricted.

Flowchart

This shows the state-changing request path. Safe methods pass at the first check unless removed with WithSafeHTTPMethods.

flowchart TD
    A[State-changing request] --> B{Configured mode and destination checks pass?}
    B -- No --> X[Reject]
    B -- Yes --> C{Sec-Fetch-Site}
    C -- cross-site --> X
    C -- same-origin --> Y[Allow]
    C -- same-site --> D{Origin matches expected origins?}
    D -- Yes --> Y
    D -- No --> X
    C -- none --> E{WithAllowSiteNone enabled?}
    E -- Yes --> Y
    E -- No --> X
    C -- missing or unknown --> F{Origin or Referer supplied?}
    F -- Yes --> G{Origin if present, otherwise Referer, is trusted?}
    G -- No --> X
    G -- Yes --> Y
    F -- No --> H{Matching missing/unknown option enabled?}
    H -- Yes --> Y
    H -- No --> I{Site missing and fallback configured?}
    I -- No --> X
    I -- Yes --> J{Fallback accepts?}
    J -- Yes --> Y
    J -- No --> X
Loading

Double-submit cookies (csrf/doublecookie)

Gimlet implements Double Submit Cookie middleware to prevent this attack.

An endpoint that is protected by this middleware requires a request that has:

  1. An HTTP CSRF reference cookie set by the server that is httpOnly
  2. An X-CSRF-Token header with a token that matches the above cookie value.

When CSRF verification fails, the middleware sets the ErrorHeader from its csrf.Namespace to csrf_token_invalid. The default header is X-CSRF-Error; a namespaced handler uses a corresponding header such as X-Endeavor-CSRF-Error. Existing response status codes and bodies are unchanged.

The idea is that you use an endpoint (such as login or a GET request to a form) to set two cookies using a csrf.TokenHandler to generate and set the cookies. The cookies are a csrf_token that can be read by Javascript on the front-end and a csrf_reference_token that is http only (e.g. cannot be read by Javascript). The front-end must take the csrf_token value and add it to the request in the X-CSRF-Token header for the request to the protected endpoint to succeed.

This package's name is csrf so you can use it as such:

import "go.rtnl.ai/gimlet/csrf/doublecookie"

// Not necessary, but maybe clearer:
import csrf "go.rtnl.ai/gimlet/csrf/doublecookie"

If multiple services share a host, configure a namespace to keep their CSRF cookies separate (see `):

handler, err := csrf.NewTokenHandlerWithNamespace(time.Hour, "/", []string{"localhost"}, secret, "application")
router.POST("/myform", csrf.DoubleCookie(handler), postForm)

The example namespace produces application_csrf_token, application_csrf_reference_token, and X-Application-CSRF-Token. Namespaces are trimmed, lowercased, and converted to safe cookie/header-name characters. An empty namespace preserves the legacy names. NewTokenHandler and existing TokenVerifier/TokenHandler implementations remain compatible; use DoubleCookieWithNamespace and SetDoubleCookieTokenWithNamespace when configuring a custom implementation.

To require signed tokens and use host-only __Host- CSRF cookies, use NewSecureTokenHandler. It requires a secret of at least 32 bytes:

handler, err := csrf.NewSecureTokenHandler(secret, "application")

There are two types of token handlers:

  1. csrf.NaiveCSRFTokens: generates cryptographically random strings
  2. csrf.SignedCSRFTokens: uses an internal secret to create an HMAC signature of the token

The Naive approach is discouraged, but is still useful. The Signed approach is much more secure as an attacker with access to the domain via another attack cannot guess the secret key and generate correct tokens.

To implement the middleware:

type Server struct {
    csrf *csrf.TokenHandler
}

func (s *Server) GetForm(c *gin.Context) {
    s.csrf.SetDoubleCookieToken(c)
}

func (s *Server) PostForm(c *gin.Context) {}

func (s *Server) Serve() {
    // Generate a random secret for signed tokens
    secret := make([]byte, 0, 65)
    rand.Read(secret)

    // Create a token handler to set double cookie tokens in the GET request.
    s.csrf = csrf.NewTokenHandler(1*time.Hour, "/", []string{"example.com"}, secret)

    // Add routes one to set the token cookies, one to verify them
    router := gin.New()
    router.GET("/myform", s.GetForm)
    router.POST("/myform", csrf.DoubleCookie(s.csrf), s.PostForm)
}

Implement csrf.TokenHandler to create your own double cookie generator and verifier.

Cache Control

The cache.Control middleware manages request and response headers for cache control by providing the user an interface to set ETags, LastModified, Expires, and Cache Control directives on objects managed by the middleware. The middleware itself will respond correctly to If-None-Match, If-Last-Modified, and If-Unmodified-Since requests and will set the Last-Modified, Expires, ETag, and Cache-Control headers on the outgoing response.

To manage a single object, create a new cache control manager as follows:

cached := router.Group("/myobj", cache.Control(cache.New("must-revalidate, private")))
{
    cached.GET("/", GetObj)
    cached.PUT("/", PutObj)
}

func GetObj(c *gin.Context) {
    c.JSON(http.StatusOk, myobj)
}

func PutObj(c *gin.Context) {
    // Set last modified timestamp and max age duration
    cache.Modified(time.Now(), 8*time.Hour)

    // Compute the ETag from the data
    data, _ := json.Marshal(myobj)
    cache.ComputeETag(data)

    c.JSON(http.StatusOk, gin.H{"success": true})
}

If you would like to manage multiple objects, with the same handler using a map, please open an GitHub issue and let us know that would be useful to you!

Note that the cache.Control takes a handler that can be very flexible. See the ETagger, Expirer, and CacheController interfaces for adding your own cache handlers to the middleware. Additionally there are built-in controllers such as Manager, ETag, WeakETag, Expires, and CacheControl that can give you more fine grained control of the cache settings.

Secure

The secure middleware provides several security enhancements to response handling and ensures that modern browser directive headers are included in the response. The middleware can be configured to determine how headers and responses are handled.

Basic Usage:

config := &secure.Config{
    ContentTypeNosniff: true,
    CrossOriginOpenerPolicy: secure.SameOrigin,
    ReferrerPolicy: secure.StrictOriginWhenCrossOrigin,
    HSTS: secure.HSTSConfig{
        Seconds: 63072000,
        IncludeSubdomains: true,
        Preload: true,
    }
}

router.Use(secure.Secure(config))

HTMX

The htmx package provides helpers for services that render server-side HTML for HTMX front-ends. It defines constants for all of the HTMX request and response headers, a middleware to restrict routes to HTMX requests, and helpers for redirects and client-side events.

HTMX Only Routes

Some endpoints return HTML partials that are only meaningful to the user interface and should not be part of your public API. Use the htmx.Only middleware to reject requests that do not have the HX-Request header with a 406 Not Acceptable error:

ui := router.Group("/ui", htmx.Only)
{
    ui.GET("/tasks", ListTasksPartial)
}

Endpoints that use this middleware should not be documented in your API documentation. If you only need to branch on the request type rather than reject it, use htmx.IsHTMXRequest(c) which returns true when the HX-Request header is set.

Redirects

HTMX swaps the response into the DOM, so a normal 302 redirect is followed by the AJAX request rather than the browser. The htmx.Redirect helper sets the HX-Redirect header and returns a 204 No Content for HTMX requests so that the client performs the redirect; for all other requests it falls back to a standard c.Redirect with the given status code.

func CreateTask(c *gin.Context) {
    // ... create the task ...
    htmx.Redirect(c, http.StatusFound, "/tasks")
}

Client-Side Events

htmx.Trigger sets the HX-Trigger response header and returns a 204 No Content so the front-end can listen for the event with hx-trigger. It returns false and does nothing if the request is not an HTMX request. The event may be a string, an error, or any fmt.Stringer:

func DeleteTask(c *gin.Context) {
    // ... delete the task ...
    htmx.Trigger(c, "task-deleted")
}

If you need to notify listeners and return content for HTMX to swap, use htmx.SetTriggerHeader, which sets the header without writing a response:

func UpdateTask(c *gin.Context) {
    // ... update the task ...
    htmx.SetTriggerHeader(c, "task-updated")
    c.HTML(http.StatusOK, "task.html", task)
}

The htmx.Event type provides a consistent naming convention for CRUD events; it renders as a lowercased name-type string such as task-created. The EventCreated, EventUpdated, and EventDeleted types have convenience helpers so you rarely need to construct an event directly:

htmx.TriggerCreated(c, "task")                     // sends HX-Trigger: task-created
htmx.TriggerUpdated(c, "task")                     // sends HX-Trigger: task-updated
htmx.TriggerDeleted(c, "task")                     // sends HX-Trigger: task-deleted
htmx.TriggerCreatedRedirect(c, "task", "/tasks")   // trigger, then redirect

Each of these returns a boolean indicating whether the request was an HTMX request and the trigger was sent; use htmx.TriggerRedirect to trigger a custom event and redirect in one step.

About

Rotational's web services built in Go use common middleware for logging, authentication, authorization, csrf protection and more. This package unifies our middleware usage across all of our web services, simplifying service development. Rotational services primarily depend on Gin as the base framework, hence gimlet - lime and gin!

About

Middleware for server applications that use the Gin web framework

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages