Learn about Parka's powerful middlewares for extracting parameters from HTTP requests, including query parameters, form data, and JSON POST requests
Parka provides powerful middlewares for extracting parameters from HTTP requests, specifically designed to work with Glazed commands. This guide explains how to use these middlewares to handle URL query parameters, form data, and JSON POST requests.
The three main middlewares for parameter extraction are:
UpdateFromQueryParameters - Extracts parameters from URL query stringsUpdateFromFormQuery - Extracts parameters from form data, including file uploadsJSONBodyMiddleware - Extracts parameters from JSON POST request bodiesThese middlewares are essential when exposing Glazed commands through HTTP endpoints, as they allow seamless translation of HTTP request data into Glazed command parameters.
The query parameter middleware extracts parameters from URL query strings and updates the Glazed command's parameter layers accordingly.
import (
"github.com/go-go-golems/parka/pkg/glazed/middlewares"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
)
// In your handler
middlewares_ := []middlewares.Middleware{
parka_middlewares.UpdateFromQueryParameters(c,
parameters.WithParseStepSource("query")),
}
Here's a complete example of using the query parameter middleware with a Glazed command:
type MyCommand struct {
*cmds.CommandDescription
}
func NewMyCommand() (*MyCommand, error) {
return &MyCommand{
CommandDescription: cmds.NewCommandDescription(
"mycommand",
cmds.WithShort("A command with query parameters"),
cmds.WithFlags(
parameters.NewParameterDefinition(
"limit",
parameters.ParameterTypeInteger,
parameters.WithHelp("Number of items to return"),
parameters.WithDefault(10),
),
parameters.NewParameterDefinition(
"filter",
parameters.ParameterTypeString,
parameters.WithHelp("Filter results"),
),
),
),
}, nil
}
// In your Echo handler
func HandleMyCommand(c echo.Context) error {
cmd := NewMyCommand()
parsedLayers := layers.NewParsedLayers()
middlewares_ := []middlewares.Middleware{
parka_middlewares.UpdateFromQueryParameters(c),
}
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
middlewares_...,
)
if err != nil {
return err
}
// Now parsedLayers contains the parameters from the query string
// e.g., /api/mycommand?limit=20&filter=active
return cmd.RunIntoGlazeProcessor(c.Request().Context(), parsedLayers, processor)
}
The form data middleware handles both regular form fields and file uploads. It's particularly useful when your command needs to process uploaded files or handle complex form data.
import (
"github.com/go-go-golems/parka/pkg/glazed/middlewares"
)
middlewares_ := []middlewares.Middleware{
parka_middlewares.UpdateFromFormQuery(c),
}
The form middleware can handle file parameters defined in your Glazed command:
func NewFileUploadCommand() (*FileUploadCommand, error) {
return &FileUploadCommand{
CommandDescription: cmds.NewCommandDescription(
"upload",
cmds.WithShort("Upload and process files"),
cmds.WithFlags(
parameters.NewParameterDefinition(
"file",
parameters.ParameterTypeStringFromFile,
parameters.WithHelp("File to process"),
parameters.WithRequired(true),
),
parameters.NewParameterDefinition(
"description",
parameters.ParameterTypeString,
parameters.WithHelp("File description"),
),
),
),
}, nil
}
Here's a complete example showing how to handle both file uploads and regular form fields:
func HandleFileUpload(c echo.Context) error {
cmd := NewFileUploadCommand()
parsedLayers := layers.NewParsedLayers()
middlewares_ := []middlewares.Middleware{
parka_middlewares.UpdateFromFormQuery(c),
}
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
middlewares_...,
)
if err != nil {
return err
}
return cmd.RunIntoGlazeProcessor(c.Request().Context(), parsedLayers, processor)
}
The JSON body middleware handles parameters sent in JSON format via POST requests. It's particularly useful for complex parameter structures and file-like parameters that contain content directly in the request body.
import (
"github.com/go-go-golems/parka/pkg/glazed/middlewares"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
)
// Create a new JSON middleware instance
jsonMiddleware := middlewares.NewJSONBodyMiddleware(c,
parameters.WithParseStepSource("json"))
defer jsonMiddleware.Close() // Important: Always close to cleanup temporary files
// Use in your middleware chain
middlewares_ := []middlewares.Middleware{
jsonMiddleware.Middleware(),
}
Here's a complete example of using the JSON body middleware with a Glazed command:
type MyCommand struct {
*cmds.CommandDescription
}
func NewMyCommand() (*MyCommand, error) {
return &MyCommand{
CommandDescription: cmds.NewCommandDescription(
"mycommand",
cmds.WithShort("A command with JSON parameters"),
cmds.WithFlags(
parameters.NewParameterDefinition(
"content",
parameters.ParameterTypeStringFromFile,
parameters.WithHelp("Content to process"),
parameters.WithRequired(true),
),
parameters.NewParameterDefinition(
"options",
parameters.ParameterTypeObject,
parameters.WithHelp("Processing options"),
),
),
),
}, nil
}
// In your Echo handler
func HandleMyCommand(c echo.Context) error {
cmd := NewMyCommand()
parsedLayers := layers.NewParsedLayers()
jsonMiddleware := middlewares.NewJSONBodyMiddleware(c)
defer jsonMiddleware.Close()
middlewares_ := []middlewares.Middleware{
jsonMiddleware.Middleware(),
middlewares.SetFromDefaults(),
}
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
middlewares_...,
)
if err != nil {
return err
}
return cmd.RunIntoGlazeProcessor(c.Request().Context(), parsedLayers, processor)
}
The JSON middleware can handle file-like parameters by creating temporary files from string content in the JSON:
{
"content": "This will be written to a temp file\nAnd processed as a file parameter",
"options": {
"format": "text",
"encoding": "utf-8"
}
}
The middleware will:
Parka provides a convenient JSON handler that can work with both query parameters and JSON body:
import (
"github.com/go-go-golems/parka/pkg/glazed/handlers/json"
)
// For query parameters (GET requests)
handler := json.CreateJSONQueryHandler(cmd,
json.WithParseOptions(parameters.WithParseStepSource("query")))
// For JSON body (POST requests)
handler := json.CreateJSONBodyHandler(cmd,
json.WithParseOptions(parameters.WithParseStepSource("json")))
// Register with Echo
e.GET("/api/command", handler)
e.POST("/api/command", handler)
The handler supports configuration through options:
WithJSONBody() - Use JSON body parsing instead of query parametersWithParseOptions() - Add parameter parse optionsWithMiddlewares() - Add additional middlewares to the chainAlways Close the Middleware: Use defer middleware.Close() to ensure temporary files are cleaned up.
Error Handling: The middleware provides detailed error messages for:
Parameter Types: The middleware supports:
Thread Safety: The middleware is thread-safe for temporary file management.
You can combine different middlewares to handle various parameter sources:
middlewares_ := []middlewares.Middleware{
// For GET requests
parka_middlewares.UpdateFromQueryParameters(c),
// For POST with form data
parka_middlewares.UpdateFromFormQuery(c),
// For POST with JSON
jsonMiddleware.Middleware(),
// Always set defaults last
middlewares.SetFromDefaults(),
}
Order Matters: Place the middlewares in the order you want them to process. Later middlewares can override values set by earlier ones.
Default Values: Always use middlewares.SetFromDefaults() as the last middleware to ensure default values are set for unspecified parameters.
Error Handling: Always check for middleware execution errors before proceeding with command execution.
Parameter Types: Be mindful of parameter types when defining your command. The middlewares will attempt to parse the input according to the parameter type.
File Handling: When dealing with file uploads:
ParameterTypeStringFromFile, ParameterTypeStringFromFiles)func HandleAPIEndpoint(c echo.Context) error {
cmd := NewAPICommand()
parsedLayers := layers.NewParsedLayers()
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
parka_middlewares.UpdateFromQueryParameters(c),
middlewares.SetFromDefaults(),
)
if err != nil {
return err
}
// Process the command
return cmd.RunIntoGlazeProcessor(c.Request().Context(), parsedLayers, processor)
}
func HandleFormSubmission(c echo.Context) error {
cmd := NewFormCommand()
parsedLayers := layers.NewParsedLayers()
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
parka_middlewares.UpdateFromFormQuery(c),
middlewares.SetFromDefaults(),
)
if err != nil {
return err
}
// Process the form submission
return cmd.RunIntoGlazeProcessor(c.Request().Context(), parsedLayers, processor)
}
func HandleAPIEndpoint(c echo.Context) error {
cmd := NewAPICommand()
parsedLayers := layers.NewParsedLayers()
jsonMiddleware := middlewares.NewJSONBodyMiddleware(c,
parameters.WithParseStepSource("json"))
defer jsonMiddleware.Close()
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
jsonMiddleware.Middleware(),
middlewares.SetFromDefaults(),
)
if err != nil {
return err
}
// Process the command
return cmd.RunIntoGlazeProcessor(c.Request().Context(), parsedLayers, processor)
}
func HandleFlexibleEndpoint(c echo.Context) error {
cmd := NewFlexibleCommand()
// Use the JSON handler which can handle both query and body
handler := json.NewQueryHandler(cmd,
json.WithParseOptions(parameters.WithParseStepSource("auto")))
if c.Request().Method == "POST" {
handler.UseJSONBody = true
}
return handler.Handle(c)
}
The DataTables handler in Parka provides a good example of how these middlewares are used in practice:
func CreateDataTablesHandler(cmd cmds.GlazeCommand, options ...QueryHandlerOption) echo.HandlerFunc {
return func(c echo.Context) error {
parsedLayers := layers.NewParsedLayers()
middlewares_ := []middlewares.Middleware{
parka_middlewares.UpdateFromQueryParameters(c),
// Add custom middlewares
middlewares.SetFromDefaults(),
}
err := middlewares.ExecuteMiddlewares(
cmd.Description().Layers.Clone(),
parsedLayers,
middlewares_...,
)
if err != nil {
return err
}
// Process the command and render the DataTables view
return nil
}
}
The query parameter middleware (UpdateFromQueryParameters) is designed to extract and parse URL query parameters into Glazed command parameters. Here's a detailed look at how it works internally:
func UpdateFromQueryParameters(c echo.Context, options ...parameters.ParseStepOption) middlewares.Middleware {
return func(next middlewares.HandlerFunc) middlewares.HandlerFunc {
return func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error {
// ... middleware implementation
}
}
}
The middleware is structured as a closure that takes an Echo context and returns a Glazed middleware function. This pattern allows it to access both the HTTP context and the Glazed parameter system.
Layer Iteration:
err := layers_.ForEachE(func(_ string, l layers.ParameterLayer) error {
parsedLayer := parsedLayers.GetOrCreate(l)
// ... process parameters
})
Parameter Extraction:
value := c.QueryParam(p.Name)
if value == "" {
if p.Required {
return errors.Errorf("required parameter '%s' is missing", p.Name)
}
return nil
}
QueryParam methodType Conversion:
parsedParameter, err := p.ParseParameter([]string{value}, options...)
if err != nil {
return errors.Wrapf(err, "invalid value for parameter '%s': %s", p.Name, value)
}
Array Parameter Handling:
if p.Type.IsList() {
values := c.QueryParams()[p.Name]
if len(values) > 0 {
parsedParameter, err := p.ParseParameter(values, options...)
// ... error handling
parsedLayer.Parameters.Update(p.Name, parsedParameter)
}
}
QueryParams() to get all values for a parameter nameThe form middleware (UpdateFromFormQuery) handles both regular form fields and file uploads. Here's a detailed look at its internal workings:
func getFileParameterFromForm(c echo.Context, p *parameters.ParameterDefinition) (interface{}, error) {
form, err := c.MultipartForm()
if err != nil {
return nil, err
}
headers := form.File[p.Name]
// ... process files
}
for _, h := range headers {
err = func() error {
f, err := h.Open()
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
v, err := p.ParseFromReader(f, h.Filename)
if err != nil {
return errors.Wrapf(err, "invalid value for parameter '%s': %s", p.Name, h.Filename)
}
values = append(values, v.Value)
return nil
}()
}
ParseFromReader for content processingfunc getListParameterFromForm(c echo.Context, p *parameters.ParameterDefinition, options ...parameters.ParseStepOption) (*parameters.ParsedParameter, error) {
if p.Type.IsList() {
values_, err := c.FormParams()
if err != nil {
return nil, err
}
values, ok := values_[fmt.Sprintf("%s[]", p.Name)]
// ... process array values
}
}
[] suffix conventionif ok {
pValue, err := p.ParseParameter(values, options...)
if err != nil {
return nil, errors.Wrapf(err, "invalid value for parameter '%s': %s", p.Name, values)
}
return pValue, nil
}
switch {
case p.Type.IsList():
vs := []interface{}{}
for _, v_ := range values {
vss, err := cast.CastListToInterfaceList(v_)
if err != nil {
return nil, err
}
vs = append(vs, vss...)
}
v = vs
case p.Type == parameters.ParameterTypeStringFromFile,
p.Type == parameters.ParameterTypeStringFromFiles:
s := ""
for _, v_ := range values {
ss, ok := v_.(string)
if !ok {
return nil, errors.Errorf("invalid value for parameter '%s': (%v) %s", p.Name, v_, "expected string")
}
s += ss
}
v = s
}
Both middlewares implement comprehensive error handling:
Required Parameters:
Type Validation:
File Processing Errors:
The JSON middleware (JSONBodyMiddleware) is designed to handle JSON POST requests and manage temporary files. Here's a detailed look at its internal workings:
type JSONBodyMiddleware struct {
c echo.Context
options []parameters.ParseStepOption
files []string
mu sync.Mutex
}
Body Reading:
body, err := io.ReadAll(m.c.Request().Body)
var jsonData map[string]interface{}
if err := json.Unmarshal(body, &jsonData); err != nil {
return errors.Wrap(err, "could not parse JSON body")
}
Parameter Extraction:
value, exists := jsonData[p.Name]
if !exists {
if p.Required {
return errors.Errorf("required parameter '%s' is missing", p.Name)
}
return nil
}
File Parameter Handling:
if p.Type.NeedsFileContent("") {
switch v := value.(type) {
case string:
tmpPath, err := m.createTempFileFromString(v)
// ... process file ...
}
}
Type Conversion:
switch v := value.(type) {
case string:
stringValue = v
case float64:
stringValue = fmt.Sprintf("%v", v)
case bool:
stringValue = fmt.Sprintf("%v", v)
case []interface{}:
// Handle arrays
}
The middleware uses a thread-safe approach to manage temporary files:
func (m *JSONBodyMiddleware) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
var errs []error
for _, f := range m.files {
if err := os.Remove(f); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to remove temporary file %s", f))
}
}
m.files = m.files[:0]
if len(errs) > 0 {
return errors.Errorf("failed to clean up some temporary files: %v", errs)
}
return nil
}
This ensures that: