Upgrading To v1.18 From v1.17
Exciting New Features 🎉
Enhancements 🚀
- Laravel-style collections
- Native validation engine and built-in rules
- Validation supports translation-based error messages and lang:publish command
- Maintenance mode commands
- Runners can be disabled via config option
- WithoutMiddleware for routes
- Build command supports go generate
- Artisan console supports table output
- Artisan console supports graceful shutdown via Shutdownable
- Artisan console supports commands filtering
- gRPC supports TLS and mTLS credentials
- Orm query exposes context
- Package views via LoadViewsFrom
- Package setup supports import aliases
- Queue drivers support batch-consuming via DriverWithReceive
- Log WithContext filters framework context keys out
- Installer supports agent skill management commands
- Fiber driver context decoupled from Fiber instance lifecycle
- Fiber driver template configuration replaced with DefaultTemplate
- Cache prefix supports empty string
Breaking Changes 🛠
- HTTP Middleware changed from func to interface
- Validation engine rewritten natively
- Mail content separates raw text from template views
- gRPC config key renamed from servers to clients
- HTTP driver timeout middleware rewritten for concurrent safety
Upgrade Guide
As Golang v1.24 is no longer maintained, the Golang version of Goravel supports has been upgraded from 1.24 to 1.25.
goravel/example project from v1.17 to v1.18 PR can be used as an upgrade reference: goravel/example#133.
You can copy and paste the following into your AI coding agent to upgrade your Goravel project automatically.
# Upgrade Guide for Goravel v1.18
**Before you begin**: ensure all changes are committed or stashed, the project builds cleanly (`go build ./...`), and Go ≥ 1.25 is installed, Goravel version ≥ 1.17.0.
## Step 1: Update Go module dependencies
**Detection**: Check which facade packages are already installed:
```shell
rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null
```
**Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`:
```shell
go get github.com/goravel/framework@latest
# For each facade found in detection, run the corresponding go get.
# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest
# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest
# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest
# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest
# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest
# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest
# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest
go mod tidy
```
## Step 2: Migrate HTTP middleware from func to interface
`contracts/http.Middleware` changed from `func(http.Context)` to an interface with `Handle(http.Context)` and `Signature() string` methods. Every custom middleware must be updated.
### 2a. Detect all custom middleware functions
```shell
rg -n 'func\s+\w+\s*\(.*\)\s+http\.Middleware' --type go
rg -n 'func\s+\w+\s*\(.*\)\s+contractshttp\.Middleware' --type go
```
Also search for anonymous middleware (inline `return func(ctx http.Context)`):
```shell
rg -n 'return\s+func\(ctx\s+http\.Context\)' --type go
```
### 2b. Convert each middleware
For each detected middleware, keep the factory function signature and replace the closure body with a struct literal:
```diff
func Auth() http.Middleware {
- return func(ctx http.Context) {
- ctx.Request().Next()
- }
+ return &Auth{}
}
+type Auth struct{}
+
+func (a *Auth) Handle(ctx http.Context) {
+ ctx.Request().Next()
+}
+
+func (a *Auth) Signature() string {
+ return "auth"
+}
```
If the original factory accepted parameters, pass them to the struct literal:
```diff
func Throttle(name string) http.Middleware {
- return func(ctx http.Context) {
- // throttle logic using name
- }
+ return &Throttle{Name: name}
}
+
+type Throttle struct{ Name string }
+
+func (t *Throttle) Handle(ctx http.Context) { /* throttle logic using t.Name */ }
+func (t *Throttle) Signature() string { return "throttle-" + t.Name }
```
Call sites like `middleware.Auth()` stay unchanged — the factory function still returns `http.Middleware`, but now returns a struct instead of a closure.
### 2c. Verification
```shell
# Ensure no remaining func-returning middleware patterns
rg -n 'func.*http\.Middleware' --type go
# Build must succeed
go build ./...
```
## Step 3: Update validation rule and filter maps
Validation rules and filters now use `map[string]any` instead of `map[string]string`.
### 3a. Detect affected files
```shell
rg -n 'map\[string\]string.*Validate\(' --type go
rg -n 'map\[string\]string.*Make\(' --type go
rg -n 'Rules\(\).*map\[string\]string' --type go
rg -n 'Filters\(\).*map\[string\]string' --type go
rg -n 'Messages\(\).*map\[string\]string' --type go
rg -n 'Attributes\(\).*map\[string\]string' --type go
```
Also search for `Validate` and `Make` calls that may already use `map[string]any` or use short variable declarations:
```shell
rg -n '\.Validate\(.*map\[string\]' --type go
rg -n 'validation\.Make\(.*map\[string\]' --type go
```
### 3b. Apply the transformation
Replace `map[string]string` with `map[string]any` in `Validate` and `Make` calls:
```diff
-ctx.Request().Validate(map[string]string{
+ctx.Request().Validate(map[string]any{
"title": "required|max:255",
})
```
Also update form request method signatures — `Rules()`, `Messages()`, `Attributes()`, and `Filters()`:
```diff
-func (r *UserCreate) Rules(ctx http.Context) map[string]string {
- return map[string]string{
+func (r *UserCreate) Rules(ctx http.Context) map[string]any {
+ return map[string]any{
"name": "required",
}
}
-func (r *UserCreate) Messages(ctx http.Context) map[string]string {
- return map[string]string{}
+func (r *UserCreate) Messages(ctx http.Context) map[string]any {
+ return map[string]any{}
}
-func (r *UserCreate) Filters(ctx http.Context) map[string]string {
- return map[string]string{
+func (r *UserCreate) Filters(ctx http.Context) map[string]any {
+ return map[string]any{
"name": "trim",
}
}
```
If a `regex` or `not_regex` rule value contains `|` and is followed by more rules, switch to `[]string`:
```diff
rules := map[string]string{
- "slug": "required|regex:^(news|docs)-[a-z]+$|string",
+ "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"},
}
```
### 3c. Verification
```shell
# Ensure no remaining map[string]string in validation contexts
rg -n 'Validate\(.*map\[string\]string' --type go
rg -n 'Make\(.*map\[string\]string' --type go
rg -n 'Rules\(.*map\[string\]string' --type go
rg -n 'Messages\(.*map\[string\]string' --type go
rg -n 'Filters\(.*map\[string\]string' --type go
go build ./...
```
## Step 4: Update mail template content fields
Mail content fields are now explicit: `HtmlView` for HTML templates, `TextView` for plain-text templates, `Html`/`Text` for raw body content.
### 4a. Detect affected files
```shell
rg -n '\.View\s*:' --type go -g '!vendor/**' -g '!storage/**'
rg -n 'Content\(.*mail\.Content\{' --type go
rg -n '&mail\.Content\{' --type go
```
### 4b. Apply the transformation
For each `mail.Content{}` literal:
| Old field | New field | Condition |
|-----------|-----------|-----------|
| `View:` | `HtmlView:` | Always (was for HTML templates) |
| `Text:` (value ends in `.html`, `.txt`, `.tmpl`, or is a file path) | `TextView:` | When the value was a template path |
| `Text:` (value is a raw string, not a path) | `Text:` | Keep as-is for raw body |
**Example — template-based email**:
```diff
Content(mail.Content{
- View: "welcome.html",
- Text: "welcome.txt",
+ HtmlView: "welcome.html",
+ TextView: "welcome.txt",
With: map[string]any{
"Name": "Goravel",
},
})
```
**Example — raw body email (no change)**:
```go
Content(mail.Content{Text: "Hello Goravel"})
```
If a file uses `Html:` / `Text:` already (raw body pattern from v1.17+), no change is needed.
### 4c. Verification
```shell
# Ensure no remaining View: in mail Content structs
rg -n '\.View\s*:.*\.html' --type go
go build ./...
```
## Step 5: Rename gRPC config key servers to clients
### 5a. Detect affected files
```shell
rg -n '"servers"' --type go -g '*grpc*'
rg -n 'grpc\.servers' --type go
```
### 5b. Apply the transformation
In `config/grpc.go`, rename the config key:
```diff
config.Add("grpc", map[string]any{
"host": config.Env("GRPC_HOST", ""),
- "servers": map[string]any{
+ "clients": map[string]any{
"user": map[string]any{
"host": config.Env("GRPC_USER_HOST", ""),
...
},
},
})
```
In application code, update any config path references:
```diff
-facades.Config().Get("grpc.servers.user.host")
+facades.Config().Get("grpc.clients.user.host")
```
### 5c. Verification
```shell
# Ensure no remaining "servers" key in grpc config
rg -n '"servers"' --type go -g '*grpc*'
rg -n 'grpc\.servers' --type go
go build ./...
```
## Step 6: Update validation error messages and custom rule names
The new native validation engine ([goravel/framework#1397](https://github.com/goravel/framework/pull/1397)) produces Laravel-style error messages and evaluates all rules (including wildcards) differently. Any tests that assert on specific validation error messages or JSON error responses must be updated.
### 6a. Detect affected tests
```shell
rg -n 'Equal.*message.*required' --type go
rg -n 'Equal.*message.*regex' --type go
rg -n 'Equal.*message.*date' --type go
rg -n 'Required.*is required to not be empty' --type go
```
### 6b. Update error message expectations
Replace old-style error message assertions with the new Laravel-style messages:
| Old message | New message |
|-------------|-------------|
| `"name is required to not be empty"` | `"The name field is required."` |
| `"code value does not pass the regex check"` | `"The code field format is invalid."` |
| `"date value should be a date string"` | `"The date field must be a valid date."` |
Example test diff:
```diff
-s.Equal("{\"message\":{\"name\":{\"required\":\"name is required to not be empty\"}}}", content)
+s.Equal("{\"message\":{\"name\":{\"required\":\"The name field is required.\"}}}", content)
```
> **Note**: Wildcard rules (e.g., `scores.*`, `tags.*`) now consistently appear in error output even when no input is provided. Add assertions for these if your tests check the full error response body.
### 6c. Rename custom rules that conflict with built-in rules
The new engine includes built-in rules (`required`, `email`, `exists`, `unique`, etc.) that may conflict with custom rule names. Rename any custom rule whose `Signature()` matches a built-in rule name:
```diff
func (receiver *Exists) Signature() string {
- return "exists"
+ return "exists_custom"
}
```
### 6d. Verification
```shell
go build ./...
go test ./... 2>&1 || true
```
## Step 7: Install the goravel-development skill
Download the Goravel AI coding conventions skill from goravel-lite so you can reference project structure, facades, testing patterns, and Artisan commands during the upgrade:
```shell
mkdir -p .agents/skills/goravel-development
curl -fsSL -o .agents/skills/goravel-development/SKILL.md \
https://raw.githubusercontent.com/goravel/goravel-lite/master/.agents/skills/goravel-development/SKILL.md
```
### 7a. Verification
```shell
ls -l .agents/skills/goravel-development/SKILL.md
```
## Step 8: Final verification
After completing all steps, run the full verification suite:
```shell
go build ./...
go vet ./...
go test ./... 2>&1 || true
```
If tests fail, the most common causes are:
- Test middleware functions that were not migrated (step 2).
- Test validation calls using `map[string]string` (step 3).
- Test mail Content structs with old field names (step 4).
- Test validation error message assertions with old format (step 6).
- Custom rule signatures conflicting with built-in rule names (step 6).Feature Introduction
AI SDK
Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, generated media, and speech workflows through one facade. Install the facade with go run .artisan package:install AI, then install a provider package such as goravel/openai, goravel/anthropic, or goravel/gemini.
The initial release includes:
- Agent conversations with
make:agent,make:tool, customizable AI generator paths,Prompt,Stream, tool calling, prompt middleware, provider failover. - Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs.
- Image and audio generation with
Store/StoreAshelpers, plus transcription with language and diarization options.
The goravel-lite scaffold also ships an AI Agent Development Skill which teaches AI coding agents (Cursor, Copilot, Codex, etc.) how to work with Goravel projects. The skill covers project structure, facades, routing, ORM, testing conventions, and Artisan scaffolding commands. When an AI agent reads this skill, it already knows how to bootstrap a Goravel project, use package:install to add facades, work with the facade pattern, write tests with testify suites and mock helpers, and use Artisan make:* generators. You can extend the skill with project-specific rules by creating .agents/skills/goravel-development/CUSTOM.md. The skill is versioned alongside goravel-lite and is updated when you upgrade the framework.
A minimal prompt looks like this:
conversation, err := facades.AI().Agent(&agents.SupportAgent{})
if err != nil {
return err
}
response, err := conversation.Prompt("How do I create a controller?")
if err != nil {
return err
}
fmt.Println(response.Text())Telemetry
Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for collecting traces, metrics, and logs and exporting them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. Install the facade with go run . artisan package:install Telemetry.
The initial release includes:
- Traces, metrics, and logs configured from a single
config/telemetry.gofile, with OTLP, console, and custom exporters, sampling, and batching controls. - Built-in instrumentation for the HTTP server, the HTTP client, gRPC, the database, and the logger, with trace context propagation across services.
- Automatic flush and shutdown when the application stops.
A manual span looks like this:
ctx, span := facades.Telemetry().Tracer("app").Start(ctx, "process-order")
defer span.End()
span.SetAttributes(telemetry.String("order.id", "1234"))
// Pass ctx down to create child spans
processItems(ctx)The Tracer argument ("app") is the instrumentation scope name that identifies the code producing the span; it is recorded as otel.scope.name. By convention it is the package import path or, for application code, the application name.
Laravel-style collections
Goravel now provides fluent eager and lazy collection utilities in the support/collect package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style Where comparisons for structs, aggregate values, and process large datasets with LazyCollection.
numbers := collect.New(1, 2, 3, 4, 5, 6)
evens := numbers.Filter(func(n int, _ int) bool {
return n%2 == 0
})
evens.All() // []int{2, 4, 6}Native validation engine and built-in rules
Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set.
validator, err := facades.Validation().Make(ctx,
map[string]any{
"email": " User@Goravel.dev ",
"scores": []int{1, 2},
},
map[string]any{
"email": "required|email",
"scores.*": "required|integer",
},
validation.Filters(map[string]any{
"email": "trim|lower",
}),
)
validated := validator.Validated()
scores := validated["scores"].([]int)Validation supports translation-based error messages and lang:publish command
Validation error messages are now powered by the framework's localization system. Default messages are embedded as lang/en/validation.json and use validation.* translation keys (e.g., validation.required, validation.min.string).
Use the new lang:publish Artisan command to publish the validation language files to your application:
go run . artisan lang:publish
go run . artisan lang:publish --forceAfter publishing, edit lang/en/validation.json to customize default error messages. Create additional language directories (e.g., lang/zh/validation.json) for other locales. Custom rule messages now support :value and :optionN placeholders.
Maintenance mode commands
Goravel now provides the artisan down and artisan up commands for application maintenance mode. The file maintenance driver is used by default, so the down command writes maintenance metadata to storage and the up command removes it.
go run . artisan down
go run . artisan upThe down command supports response customization with options such as --reason, --status, --redirect, --render, --secret, and --with-secret.
go run . artisan down --reason="Upgrading database" --status=503
go run . artisan down --redirect=/maintenance
go run . artisan down --render=errors/503
go run . artisan down --with-secretMaintenance settings are read from app.maintenance in config/app.go. New Route installations add this configuration automatically. Existing applications can add it manually:
"maintenance": map[string]any{
"driver": config.Env("APP_MAINTENANCE_DRIVER", "file"),
"store": config.Env("APP_MAINTENANCE_STORE", ""),
},For multi-server deployments, set APP_MAINTENANCE_DRIVER=cache to store maintenance state in a shared cache store. Use APP_MAINTENANCE_STORE to choose a named cache store, such as Redis:
APP_MAINTENANCE_DRIVER=cache
APP_MAINTENANCE_STORE=redisRunners can be disabled via config option
Goravel v1.18 replaces the internal app.auto_run flag with a public, glob-aware app.disabled_runners config option. It lets you selectively skip auto-run runners in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping goravel:http, goravel:queue, and goravel:grpc runners.
The value is a slice of glob patterns matched against each runner's signature with Go's path.Match:
// config/app.go
"app": map[string]any{
"env": "production",
"debug": false,
"disabled_runners": []string{"goravel:schedule"},
},The framework runner signatures are goravel:http, goravel:grpc, goravel:queue, goravel:schedule, and goravel:telemetry. Patterns are evaluated in order and the first match wins, so you can mix exact and wildcard matches:
// Web container — only skip the scheduler
"disabled_runners": []string{"goravel:schedule"},
// Scheduler-only container — disable http, queue, grpc
"disabled_runners": []string{"goravel:http", "goravel:queue", "goravel:grpc"},
// Disable every framework runner
"disabled_runners": []string{"goravel:*"},
// Kill switch: run no auto-run runners in the main process
"disabled_runners": []string{"*"},Invalid patterns are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. The filter runs centrally in foundation.configureRunners(), so any new runner added in the future picks up the same behavior for free.
WithoutMiddleware for routes
Goravel v1.18 introduces the WithoutMiddleware method for both routes and route groups, similar to Laravel's withoutMiddleware(). This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting.
Exclude middleware on individual routes from within a middleware group:
facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) {
router.Get("/dashboard", dashboardController.Index)
// This route excludes the throttle middleware
router.Get("/api/webhook", webhookController.Handle).
WithoutMiddleware(middleware.Throttle("global"))
})Exclude middleware for all routes within a group:
facades.Route().Middleware(middleware.Auth()).
WithoutMiddleware(middleware.Auth()).
Group(func(router route.Router) {
router.Get("/public", publicController.Index)
})Note: Middleware exclusion uses the
Signature()method to identify middlewares. Make sure each middleware returns a unique signature forWithoutMiddlewareto work correctly. The built-in framework middlewares already provide unique signatures.
Build command supports go generate
The build command now supports the --generate flag and -g alias. When enabled, Goravel runs go generate ./... before compiling the binary, making it easier to include generated code in the build workflow without changing the default behavior.
go run . artisan build --generate
go run . artisan build -gArtisan console supports table output
Goravel now provides the ctx.Table method for rendering structured command output. It accepts table headers, table rows, and an optional console.TableOption for border, style, column, width, and height customization.
func (receiver *ReportCommand) Handle(ctx console.Context) error {
headers := []string{"ID", "Name", "Status"}
rows := [][]string{
{"1", "Goravel", "Active"},
{"2", "Framework", "Ready"},
}
ctx.Table(headers, rows)
return nil
}Artisan console supports graceful shutdown via Shutdownable
Console commands can now opt into graceful shutdown by implementing the optional console.Shutdownable interface. The framework wires a signal.NotifyContext through every command, so pressing Ctrl+C (or sending SIGTERM) cancels the context passed to Handle. If the command also implements Shutdown(ctx Context) error, the framework calls it with a fresh context and a 30s budget for cleanup before the process exits. Handle runs in a goroutine, so the signal response is immediate.
console.Context now embeds context.Context, so commands can use <-ctx.Done() directly, pass ctx to functions that expect context.Context, and call ctx.Deadline() / ctx.Err() / ctx.Value(key) without an accessor. The schedule:run command is the canonical example — it implements Shutdown to forward cleanup to schedule.Shutdown(ctx).
package commands
import (
"errors"
"net/http"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
)
type Serve struct {
server *http.Server
}
func (r *Serve) Signature() string { return "serve" }
func (r *Serve) Description() string { return "Start the HTTP server" }
func (r *Serve) Extend() command.Extend {
return command.Extend{Category: "server"}
}
func (r *Serve) Handle(ctx console.Context) error {
if err := r.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func (r *Serve) Shutdown(ctx console.Context) error {
ctx.Info("received signal, shutting down gracefully...")
return r.server.Shutdown(ctx)
}Commands that do not implement Shutdownable keep the original behavior — the process exits as soon as the signal is received.
Artisan console supports commands filtering
ApplicationBuilder now provides a WithCommandsFilter method to scope which built-in Artisan commands are registered — useful for production binaries that don't need make:*, package:*, or vendor:publish. The callback returns a positive list of command signatures to keep, with support for exact match and glob (*) matching:
func Boot() contractsfoundation.Application {
return foundation.Setup().
WithCommands(Commands).
WithCommandsFilter(func() []string {
if facades.Config().GetString("app.env") == "production" {
return []string{
"up", "down", "key:generate", "about",
"schedule:*",
"queue:*",
}
}
return nil // keep everything in other environments
}).
WithConfig(config.Boot).
Create()
}gRPC supports TLS and mTLS credentials
Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use WithGrpcServerCredentials to enable TLS or mTLS for the server, and WithGrpcClientCredentials to register client credential groups that can be referenced by grpc.clients.<name>.credentials.
return foundation.Setup().
WithGrpcClientCredentials(func() map[string]credentials.TransportCredentials {
userCreds, err := credentials.NewClientTLSFromFile("storage/certs/ca.crt", "user.example.com")
if err != nil {
panic(err)
}
return map[string]credentials.TransportCredentials{
"user": userCreds,
}
}).
Create()Orm query exposes context
Orm queries now provide a Context() accessor. Code that receives a contractsorm.Query, such as model global scopes, can read the context passed by facades.Orm().WithContext(ctx).
"tenant": func(query contractsorm.Query) contractsorm.Query {
tenantID, _ := query.Context().Value("tenant_id").(uint)
return query.Where("tenant_id", tenantID)
}Package views via LoadViewsFrom
Extension packages can now register their own view directories with facades.View().LoadViewsFrom(path). The application's resources/views directory takes priority, and registered package views serve as defaults. The Exist method also checks registered package view paths.
// In an extension package's service_provider.go
func (r *ServiceProvider) Boot(app foundation.Application) {
facades.View().LoadViewsFrom("/path/to/package/views")
}Note: If you want to use this feature in the fiber driver, please check Fiber driver template configuration replaced with DefaultTemplate.
Package setup supports import aliases
Package setup modify helpers now support import aliases in the "<alias> <import-path>" format. This keeps generated bootstrap files readable when package import paths are long or when the package should be referenced with a custom name.
serviceProvider := "&admin.ServiceProvider{}"
moduleImport := "admin " + setup.Paths().Module().Import()
setup.Install(
modify.RegisterProvider(moduleImport, serviceProvider),
).Uninstall(
modify.UnregisterProvider(moduleImport, serviceProvider),
).Execute()Queue drivers support batch-consuming via DriverWithReceive
Custom queue drivers can now optionally implement the DriverWithReceive interface (goravel/framework#1435) to support blocking batch message consumption. When a driver implements this interface, the queue worker automatically uses Receive instead of Pop, reducing polling overhead for high-throughput drivers like Kafka or RabbitMQ.
import (
"context"
"github.com/goravel/framework/contracts/queue"
)
type KafkaDriver struct{}
// Base Driver interface methods (required)
func (d *KafkaDriver) Driver() string { return "kafka" }
func (d *KafkaDriver) Push(task queue.Task, queue string) error { return nil }
func (d *KafkaDriver) Pop(queue string) (queue.ReservedJob, error) { return nil, nil }
// Optional DriverWithReceive for batch consumption
func (d *KafkaDriver) Receive(ctx context.Context, queue string, count int) ([]queue.ReservedJob, error) {
// blocking batch receive up to `count` jobs, returns when ctx expires
}When Receive is available, the worker runs a blocking batch loop with a 5-second per-call timeout and exponential backoff (100ms–3.2s) on errors or empty batches. The context.Context is canceled on worker shutdown, ensuring clean termination. No application code changes are required for existing drivers.
Log WithContext filters framework context keys out
Previously, facades.Log().WithContext(ctx) could write framework-internal context keys such as GoravelAuthJwt and goravel_http_client_name to log output. Goravel now excludes these keys by default, and you can add logging.context.exclude for project-specific context keys.
Installer supports agent skill management commands
The goravel CLI now supports goravel skill:list and goravel skill:install for discovering and installing Goravel agent skills. These commands are designed for contributors who want to install AI coding assistant skills to help follow Goravel development conventions such as testing, naming, and pull request workflows.
Skills are sourced from the goravel/agents repository and installed to ~/.agents/skills by default. You can filter by name, choose a custom destination with --path, and overwrite existing skills with --force.
Fiber driver context decoupled from Fiber instance lifecycle
When a request timed out with the Fiber driver, the context.Context interface methods (Done(), Deadline(), Err()) delegated to the underlying fiber.Ctx, which became invalid after the request was recycled by fasthttp. This caused nil-pointer panics when background goroutines (e.g. database/sql) still held a reference to the request context (goravel/goravel#951).
The fix (goravel/fiber#253) stores the user context and values as struct fields on the goravel Context struct instead of delegating to fiber.Ctx, so timeout signals propagate correctly and background goroutines can safely access the context after the request returns.
No application code changes are required.
Fiber driver template configuration replaced with DefaultTemplate
To support package views via LoadViewsFrom, the Fiber driver (goravel/fiber#273) has replaced its dependency on gofiber/template/html/v3 with a built-in DefaultTemplate() that loads views from resources/views and any registered package views automatically. If your config/http.go has a custom template configuration using html.New(path.Resource("views"), ".tmpl") and you want to use the Package views via LoadViewsFrom feature, update it to use goravelfiber.DefaultTemplate() instead:
// config/http.go — fiber driver
import goravelfiber "github.com/goravel/fiber"
"template": func() (fiber.Views, error) {
- return html.New(path.Resource("views"), ".tmpl"), nil
+ return goravelfiber.DefaultTemplate(), nil
},For custom delimiters or FuncMap, use goravelfiber.NewTemplate(goravelfiber.RenderOptions{...}):
import goravelfiber "github.com/goravel/fiber"
"template": func() (fiber.Views, error) {
return goravelfiber.NewTemplate(goravelfiber.RenderOptions{
Delims: &goravelfiber.Delims{Left: "{[", Right: "]}"},
})
},If no template configuration is defined, DefaultTemplate() is used automatically — no code change is required.
Cache prefix supports empty string
The Redis cache driver now respects an empty prefix configuration. Previously, even with an empty prefix, a colon (:) separator was always prepended to every key, making keys inconsistent when accessing Redis directly. Now, if the prefix option in config/cache.go is set to an empty string, no prefix or separator is applied:
// config/cache.go
"prefix": "",With this configuration, a cache key like users is stored as users in Redis, rather than :users.
No application code changes are required for existing projects with a non-empty prefix.
HTTP Middleware changed from func to interface
contracts/http.Middleware has changed from a function type func(Context) to an interface:
type Middleware interface {
Handle(Context)
Signature() string
}This change provides each middleware with a stable identity via the Signature() method, enabling WithoutMiddleware to reliably exclude specific middleware by signature comparison. All built-in framework middlewares (Cors, Timeout, Throttle, VerifyCsrfToken, etc.) have been updated to return structs that implement this interface.
Custom middleware must be updated to implement the new interface. See the upgrade guide for migration steps.
Validation engine rewritten natively
The validation engine was rewritten in goravel/framework#1397 — replacing the external gookit/validate dependency with a native implementation that is fully compatible with Laravel's validation API. This change introduces several downstream effects:
Error message format changed from old-style messages (e.g.,
"name is required to not be empty") to Laravel-style messages (e.g.,"The name field is required."). Tests that assert on specific validation error messages must be updated.Wildcard rules (
*.) now consistently report errors. The old engine might have skipped wildcard-field errors depending on input; the new engine always evaluates and reports them.Form request method signatures changed —
Rules(),Messages(),Attributes(), andFilters()now returnmap[string]anyinstead ofmap[string]string.Custom validation rule names may conflict with new built-in rules. Rename custom rules that share names with built-in rules (e.g.,
exists→exists_custom).
Error messages are now powered by the localization system. Default messages are embedded as lang/en/validation.json and use validation.* translation keys. Use go run . artisan lang:publish to publish and customize them.
Mail content separates raw text from template views
Goravel now treats mail.Content.Text as raw plain-text email body content. Template paths are explicit: use HtmlView for HTML templates and TextView for plain-text templates.
Use Html and Text when the message body is already available:
func (m *WelcomeMail) Content() *mail.Content {
return &mail.Content{
Html: "<h1>Hello Goravel</h1>",
Text: "Hello Goravel",
}
}Use HtmlView and TextView when the message body should be rendered from templates:
func (m *WelcomeMail) Content() *mail.Content {
return &mail.Content{
HtmlView: "welcome.html",
TextView: "welcome.txt",
With: map[string]any{
"Name": "Goravel",
},
}
}gRPC config key renamed from servers to clients
The gRPC configuration key servers has been renamed to clients in config/grpc.go to accurately reflect that these entries register gRPC clients (consumers of remote services) rather than servers.
In config/grpc.go, rename the top-level key:
config.Add("grpc", map[string]any{
"host": config.Env("GRPC_HOST", ""),
- "servers": map[string]any{
+ "clients": map[string]any{
"user": map[string]any{
"host": config.Env("GRPC_USER_HOST", ""),
...
},
},
})In application code, update any config path references from grpc.servers to grpc.clients:
-facades.Config().Get("grpc.servers.user.host")
+facades.Config().Get("grpc.clients.user.host")HTTP driver timeout middleware rewritten for concurrent safety
The timeout middleware in both the Gin driver (goravel/gin#222) and the Fiber driver (goravel/fiber#263) has been rewritten to fix a concurrency bug (goravel#966). The previous implementation ran the handler chain in a background goroutine and could return a timeout response while the handler kept running on request-scoped state that was already eligible for reuse, causing cross-request response contamination under high concurrency.
Both drivers now delegate to their framework's native timeout middleware (gin-contrib/timeout for Gin, Fiber's built-in timeout for Fiber), which properly abandons timed-out request contexts instead of recycling them. The Timeout(...) middleware signature is unchanged and ctx.Done() still reflects request deadlines — no application code changes are required.
