kataras/iris · warning

ErrPreconditionFailed

ErrPreconditionFailed

Error message

method: %w

What it means

Returned by Context.CheckIfModifiedSince when the request method is neither GET nor HEAD, since conditional headers like If-Modified-Since are only defined for safe retrieval methods. The library wraps the sentinel ErrPreconditionFailed so callers can detect that caching logic was skipped due to a method precondition rather than a header parsing problem.

Source

Thrown at context/context.go:3476

var ErrPreconditionFailed = errors.New("precondition failed")

// CheckIfModifiedSince checks if the response is modified since the "modtime".
// Note that it has nothing to do with server-side caching.
// It does those checks by checking if the "If-Modified-Since" request header
// sent by client or a previous server response header
// (e.g with WriteWithExpiration or HandleDir or Favicon etc.)
// is a valid one and it's before the "modtime".
//
// A check for !modtime && err == nil is necessary to make sure that
// it's not modified since, because it may return false but without even
// had the chance to check the client-side (request) header due to some errors,
// like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero
// or if parsing time from the header failed. See `ErrPreconditionFailed` too.
//
// It's mostly used internally, e.g. `context#WriteWithExpiration`.
func (ctx *Context) CheckIfModifiedSince(modtime time.Time) (bool, error) {
	if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead {
		return false, fmt.Errorf("method: %w", ErrPreconditionFailed)
	}
	ims := ctx.GetHeader(IfModifiedSinceHeaderKey)
	if ims == "" || IsZeroTime(modtime) {
		return false, fmt.Errorf("zero time: %w", ErrPreconditionFailed)
	}
	t, err := ParseTime(ctx, ims)
	if err != nil {
		return false, err
	}
	// sub-second precision, so
	// use mtime < t+1s instead of mtime <= t to check for unmodified.
	if modtime.UTC().Before(t.Add(1 * time.Second)) {
		return false, nil
	}
	return true, nil
}

// WriteNotModified sends a 304 "Not Modified" status code to the client,

View on GitHub (pinned to 7bedaf55a0)

Solutions

  1. Only call CheckIfModifiedSince when ctx.Method() is GET or HEAD; short-circuit otherwise.
  2. Split handlers per method so caching logic runs only on retrieval routes.
  3. Check errors.Is(err, context.ErrPreconditionFailed) and skip caching rather than failing the request.
  4. For non-GET caching, use ETag/If-None-Match semantics manually.

Example fix

// before
if modified, err := ctx.CheckIfModifiedSince(modtime); err != nil { return err } else if !modified { ctx.NotModified(); return nil }
// after
if m := ctx.Method(); m == http.MethodGet || m == http.MethodHead {
    if modified, err := ctx.CheckIfModifiedSince(modtime); err == nil && !modified {
        ctx.NotModified()
        return nil
    }
}
Defensive patterns

Strategy: validation

Validate before calling

m := ctx.Method()
if m != http.MethodGet && m != http.MethodHead {
    // skip conditional caching entirely
}

Type guard

func cacheableMethod(ctx *context.Context) bool {
    m := ctx.Method()
    return m == http.MethodGet || m == http.MethodHead
}

Try / catch

modified, err := ctx.CheckIfModifiedSince(modtime)
if errors.Is(err, context.ErrPreconditionFailed) {
    // not GET/HEAD or no header: serve full response, ignore
} else if err != nil {
    return err
} else if !modified {
    ctx.NotModified()
    return nil
}

Prevention

When it happens

Trigger: Calling ctx.CheckIfModifiedSince(modtime) inside a handler that also serves POST/PUT/DELETE/PATCH requests, or from WriteWithExpiration on a non-GET/HEAD route.

Common situations: Shared handler registered for multiple methods (e.g. app.Any) doing conditional caching; POST responses with cache semantics; method-agnostic middleware.

Related errors


AI-assisted analysis of kataras/iris@7bedaf55a0 (2026-08-30). Data as JSON: /api/errors/15241f802af7dce9. Report an issue: GitHub.