Guida HTTP

Markdown
Complete guide to HTTP transport in Goa - routing, content negotiation, WebSocket, SSE, CORS, and static content.

Questa guida copre le caratteristiche specifiche di HTTP in Goa, dal routing di base ad argomenti avanzati come lo streaming WebSocket e la negoziazione dei contenuti.

Instradamento HTTP

Instradamento di base

Le rotte vengono definite utilizzando la funzione HTTP all’interno di un servizio:

var _ = Service("calculator", func() {
    HTTP(func() {
        Path("/calculator")  // Base path for all endpoints
    })

    Method("add", func() {
        Payload(func() {
            Field(1, "a", Int, "First operand")
            Field(2, "b", Int, "Second operand")
        })
        Result(Int)
        HTTP(func() {
            POST("/add")  // POST /calculator/add
        })
    })
})

Goa supporta tutti i metodi HTTP standard: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE.

Un singolo metodo può gestire più metodi o percorsi HTTP:

Method("manage_user", func() {
    Payload(User)
    Result(User)
    HTTP(func() {
        POST("/users")          // Create
        PUT("/users/{user_id}") // Update
        Response(StatusOK)
        Response(StatusCreated)
    })
})

Parametri del percorso

Cattura i valori dinamici dall’URL:

Method("get_user", func() {
    Payload(func() {
        Field(1, "user_id", String, "User ID")
    })
    Result(User)
    HTTP(func() {
        GET("/users/{user_id}")  // Maps to payload.UserID
    })
})

Mappare i nomi dei parametri dell’URL con i nomi dei campi del payload:

Method("get_user", func() {
    Payload(func() {
        Field(1, "id", Int, "User ID")
    })
    HTTP(func() {
        GET("/users/{user_id:id}")  // URL uses user_id, maps to payload.ID
    })
})

Parametri della query

Definire i parametri della query con la funzione Param:

Method("list_users", func() {
    Payload(func() {
        Field(1, "page", Int, "Page number", func() {
            Default(1)
            Minimum(1)
        })
        Field(2, "per_page", Int, "Items per page", func() {
            Default(20)
            Minimum(1)
            Maximum(100)
        })
    })
    Result(CollectionOf(User))
    HTTP(func() {
        GET("/users")
        Param("page")
        Param("per_page")
    })
})

Caratteri jolly

Cattura tutti i segmenti di percorso rimanenti:

Method("serve_files", func() {
    Payload(func() {
        Field(1, "path", String, "Path to file")
    })
    HTTP(func() {
        GET("/files/*path")  // Matches /files/docs/image.png
    })
})

Relazioni di servizio

Utilizzare Parent per stabilire gerarchie di servizi:

var _ = Service("users", func() {
    HTTP(func() {
        Path("/users/{user_id}")
        CanonicalMethod("get")  // Override default "show"
    })
    
    Method("get", func() {
        Payload(func() {
            Field(1, "user_id", String)
        })
        HTTP(func() {
            GET("")  // GET /users/{user_id}
        })
    })
})

var _ = Service("posts", func() {
    Parent("users")  // Inherit parent's path
    
    Method("list", func() {
        // user_id inherited from parent
        HTTP(func() {
            GET("/posts")  // GET /users/{user_id}/posts
        })
    })
})

Gerarchia dei prefissi dei percorsi

Combinare i prefissi a livello di API e di servizio:

var _ = API("myapi", func() {
    HTTP(func() {
        Path("/api")  // Global prefix
    })
})

var _ = Service("users", func() {
    HTTP(func() {
        Path("/v1/users")  // Service prefix
    })
    
    Method("show", func() {
        HTTP(func() {
            GET("/{id}")  // Final: /api/v1/users/{id}
        })
    })
})

Richieste di moduli con codifica URL

Usa FormRequest() nell’espressione HTTP di un metodo quando l’endpoint richiede application/x-www-form-urlencoded, per esempio un endpoint di token OAuth o un modulo del browser. Goa genera il codificatore client e il decodificatore server tipizzati; non servono funzioni di codec personalizzate. Le risposte mantengono la codifica esistente.

Questo metodo riceve un identificatore di account nel percorso, un identificatore di richiesta in un header e due campi di modulo nel corpo:

Method("update_labels", func() {
    Payload(func() {
        Field(1, "accountId", String, "Account di cui aggiornare le etichette")
        Field(2, "requestId", String, "Identificatore per correlare la richiesta")
        Field(3, "label", String, "Etichetta visualizzata per l’account")
        Field(4, "tags", ArrayOf(String), "Tag assegnati all’account", func() {
            MinLength(1)
        })
        Required("accountId", "requestId", "label", "tags")
    })
    HTTP(func() {
        POST("/accounts/{account_id}/labels")
        Param("accountId:account_id")
        Header("requestId:X-Request-ID")
        FormRequest()
        Body(func() {
            Attribute("label:display_name", String)
            Attribute("tags:tag", ArrayOf(String))
            Required("label", "tags")
        })
        Response(StatusNoContent)
    })
})

Param("accountId:account_id") e Header("requestId:X-Request-ID") escludono questi attributi dal corpo. Le mappature Body mantengono i nomi del payload label e tags, scegliendo però le chiavi di modulo display_name e tag. Il corpo può quindi essere:

display_name=Hello+Goa&tag=go&tag=api

Per i campi rinominati in un’espressione Body esplicita, dichiara esplicitamente i tipi e i campi obbligatori del corpo; l’ereditarietà implicita dei tipi usa i nomi esatti degli attributi.

  • Il corpo finale deve essere un oggetto non vuoto con campi primitivi o array di primitivi. Sono supportati i tipi con nome che usano queste forme. Oggetti annidati, mappe, Any e tipi di campo Go personalizzati vengono rifiutati durante la valutazione del design.
  • Gli array usano chiavi ripetute, come tag=go&tag=api. Gli array obbligatori richiedono almeno un valore trasmesso: un modulo non può rappresentare un array presente senza valori. I byte usano base64 standard.
  • Ogni chiave scalare può comparire una sola volta. Una stringa vuota è un valore presente, diverso da un campo omesso. Continuano ad applicarsi valori predefiniti e validazioni dichiarati.
  • Il server legge i campi del corpo soltanto dal corpo; i parametri di query non possono fornire valori di modulo mancanti. Tipi di contenuto errati, valori non validi, chiavi scalari ripetute ed errori di validazione vengono rifiutati prima dell’invocazione del servizio. Sono consentiti campi di modulo sconosciuti.
  • FormRequest() si usa negli endpoint HTTP ordinari. Non può essere combinato con JSON-RPC, MultipartRequest() o SkipRequestBodyEncodeDecode().

Rigenera dopo aver aggiunto FormRequest(), usando una versione di Goa che includa questo DSL. Passare un endpoint esistente da JSON ai moduli cambia il contratto di trasporto: aggiorna e distribuisci client e server insieme. L’OpenAPI generato descrive il tipo di contenuto e i nomi dei campi mappati. Il middleware dell’applicazione resta responsabile dei limiti di dimensione delle richieste.


Negoziazione del contenuto

Codificatori integrati

Gli encoder predefiniti di Goa supportano:

  • JSON (application/json, *+json)
  • XML (application/xml, *+xml)
  • Gob (application/gob, *+gob)
  • HTML (text/html)
  • Testo normale (text/plain)

Il tipo di contenuto della risposta è determinato da:

  1. Accept intestazione
  2. Content-Type header (se non c’è Accept)
  3. Predefinito (JSON)

Imposta un tipo di contenuto di risposta predefinito:

Method("create", func() {
    HTTP(func() {
        POST("/media")
        Response(StatusCreated, func() {
            ContentType("application/json")
        })
    })
})

Codificatori personalizzati

Creare codificatori personalizzati per formati specializzati:

type MessagePackEncoder struct {
    w http.ResponseWriter
}

func (enc *MessagePackEncoder) Encode(v interface{}) error {
    enc.w.Header().Set("Content-Type", "application/msgpack")
    return msgpack.NewEncoder(enc.w).Encode(v)
}

func NewMessagePackEncoder(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
    return &MessagePackEncoder{w: w}
}

Registrare i codificatori personalizzati durante la creazione del server:

func main() {
    decoder := func(r *http.Request) goahttp.Decoder {
        switch r.Header.Get("Content-Type") {
        case "application/msgpack":
            return NewMessagePackDecoder(r)
        default:
            return goahttp.RequestDecoder(r)
        }
    }
    
    encoder := func(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
        if accept := ctx.Value(goahttp.AcceptTypeKey).(string); accept == "application/msgpack" {
            return NewMessagePackEncoder(ctx, w)
        }
        return goahttp.ResponseEncoder(ctx, w)
    }
    
    server := myapi.NewServer(endpoints, mux, decoder, encoder, nil, nil)
}

Integrazione WebSocket

Recapito della progettazione: Lo streaming è definito a livello di progetto utilizzando StreamingPayload e StreamingResult. Il DSL è indipendente dal trasporto: lo stesso progetto funziona per HTTP (WebSocket/SSE) e gRPC. Si veda [DSL Reference: Streaming] (dsl-reference/#streaming) per i modelli di progettazione. Questa sezione tratta l’implementazione di WebSocket specifica per HTTP.

WebSocket consente la comunicazione bidirezionale in tempo reale. Goa implementa WebSocket attraverso il suo DSL di streaming.

Modelli di streaming

Streaming da client a server:

Method("listener", func() {
    StreamingPayload(func() {
        Field(1, "message", String, "Message content")
        Required("message")
    })
    HTTP(func() {
        GET("/listen")  // WebSocket endpoints must use GET
    })
})

Flusso da server a client:

Method("subscribe", func() {
    StreamingResult(func() {
        Field(1, "message", String, "Update content")
        Field(2, "action", String, "Action type")
        Field(3, "timestamp", String, "When it happened")
        Required("message", "action", "timestamp")
    })
    HTTP(func() {
        GET("/subscribe")
    })
})

Flusso bidirezionale:

Method("echo", func() {
    StreamingPayload(func() {
        Field(1, "message", String, "Message to echo")
        Required("message")
    })
    StreamingResult(func() {
        Field(1, "message", String, "Echoed message")
        Required("message")
    })
    HTTP(func() {
        GET("/echo")
    })
})

Implementazione di WebSocket

Implementazione lato server:

func (s *service) handleStream(ctx context.Context, stream Stream) error {
    connID := generateConnectionID()
    s.registerConnection(connID, stream)
    defer s.cleanupConnection(connID)

    errChan := make(chan error, 1)
    go func() {
        errChan <- s.handleIncoming(stream)
    }()

    select {
    case <-ctx.Done():
        return ctx.Err()
    case err := <-errChan:
        return err
    }
}

Gestione delle connessioni:

type ConnectionManager struct {
    connections map[string]*ManagedConnection
    mu          sync.RWMutex
}

func (cm *ConnectionManager) AddConnection(id string, stream Stream) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    cm.connections[id] = &ManagedConnection{
        ID:       id,
        Stream:   stream,
        LastPing: time.Now(),
    }
}

Eventi inviati dal server

Ripresa della progettazione: SSE utilizza StreamingResult a livello di progettazione con ServerSentEvents() nella mappatura HTTP. Vedere DSL Reference: Streaming per i modelli di progettazione.

SSE fornisce uno streaming unidirezionale da server a client su HTTP. È ideale per:

  • Notifiche in tempo reale
  • Feed di dati in tempo reale
  • Aggiornamenti sullo stato di avanzamento
  • Streaming di eventi

Progettazione SSE

var Event = Type("Event", func() {
    Attribute("message", String, "Message body")
    Attribute("timestamp", Int, "Unix timestamp")
    Required("message", "timestamp")
})

var _ = Service("sse", func() {
    Method("stream", func() {
        StreamingResult(Event)
        HTTP(func() {
            GET("/events/stream")
            ServerSentEvents()  // Use SSE instead of WebSocket
        })
    })
})

Personalizzare gli eventi SSE:

var Event = Type("Event", func() {
    Attribute("message", String, "Message body")
    Attribute("type", String, "Event type")
    Attribute("id", String, "Event ID")
    Attribute("retry", Int, "Reconnection delay in ms")
    Required("message", "type", "id")
})

Method("stream", func() {
    StreamingResult(Event)
    HTTP(func() {
        GET("/events/stream")
        ServerSentEvents(func() {
            SSEEventData("message")
            SSEEventType("type")
            SSEEventID("id")
            SSEEventRetry("retry")
        })
    })
})

Gestione di Last-Event-Id per flussi riprendibili:

Method("stream", func() {
    Payload(func() {
        Attribute("startID", String, "Last event ID received")
    })
    StreamingResult(Event)
    HTTP(func() {
        GET("/events/stream")
        ServerSentEvents(func() {
            SSERequestID("startID")  // Maps Last-Event-Id header
        })
    })
})

Implementazione SSE

func (s *Service) Stream(ctx context.Context, stream sse.StreamServerStream) error {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            event := &sse.Event{
                Message:   "Hello from server!",
                Timestamp: time.Now().Unix(),
            }
            if err := stream.Send(event); err != nil {
                return err
            }
        case <-ctx.Done():
            return nil
        }
    }
}

Browser client:

const eventSource = new EventSource('/events/stream');

eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Received:', data);
};

eventSource.onerror = (error) => {
    console.error('EventSource failed:', error);
    eventSource.close();
};

Configurazione CORS

Il plugin CORS gestisce le richieste di origine incrociata. Importarlo:

import (
    cors "goa.design/plugins/v3/cors/dsl"
    . "goa.design/goa/v3/dsl"
)

CORS a livello di API:

var _ = API("calc", func() {
    cors.Origin("http://127.0.0.1", func() {
        cors.Headers("X-Shared-Secret")
        cors.Methods("GET", "POST")
        cors.Expose("X-Time")
        cors.MaxAge(600)
        cors.Credentials()
    })
})

CORS a livello di servizio:

var _ = Service("calc", func() {
    // Allow specific origin
    cors.Origin("localhost")

    // Allow subdomain pattern
    cors.Origin("*.domain.com", func() {
        cors.Headers("X-Shared-Secret", "X-Api-Version")
        cors.MaxAge(100)
        cors.Credentials()
    })

    // Allow all origins
    cors.Origin("*")

    // Allow regex pattern
    cors.Origin("/.*domain.*/", func() {
        cors.Headers("*")
        cors.Methods("GET", "POST")
        cors.Expose("X-Time")
    })
})

Contenuto statico

Ripresa della progettazione: Il servizio di file statici utilizza la funzione DSL Files. Si tratta di una funzione solo HTTP. Vedere [DSL Reference: Static Files] (dsl-reference/#file-statici) per i modelli di progettazione.

Servite i file statici usando la funzione Files:

var _ = Service("web", func() {
    // Serve files from a directory
    Files("/static/{*path}", "./public")
    
    // Serve a specific file
    Files("/favicon.ico", "./public/favicon.ico")
})

Per le applicazioni a pagina singola, servire l’index.html per tutti i percorsi:

var _ = Service("spa", func() {
    // API endpoints
    Method("api", func() {
        HTTP(func() {
            GET("/api/data")
        })
    })
    
    // Serve SPA - catch-all for client-side routing
    Files("/{*path}", "./dist/index.html")
})


Vedi anche


Migliori pratiche

Progettazione degli URL

  • Utilizzare sostantivi per le risorse: /articles, non /list-articles
  • Utilizzare in modo coerente i sostantivi plurali
  • Lasciare che siano i metodi HTTP a definire le azioni
  • Mantenere gli URL gerarchici e prevedibili

Gestione degli errori

  • Mappare gli errori con gli appropriati codici di stato HTTP
  • Utilizzare formati di risposta agli errori coerenti
  • Includere messaggi di errore significativi

Prestazioni

  • Utilizzare dimensioni di buffer appropriate per WebSocket
  • Implementare il pooling delle connessioni per i servizi ad alto traffico
  • Considerare il batching dei messaggi per gli endpoint di streaming

Sicurezza

  • Utilizzare sempre HTTPS in produzione
  • Configurare CORS in modo appropriato
  • Convalidare tutti i parametri di input
  • Impostare timeout appropriati per le connessioni di lunga durata