Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pkg/clientgen/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,10 +740,19 @@ func (g *golang) rpcCallSite(rpc *meta.RPC) (code []Code, err error) {

enc := g.enc.NewPossibleInstance("respDecoder")
for _, field := range respEnc.HeaderParameters {
var getValue Code
if strings.ToLower(field.WireFormat) == "set-cookie" && field.Type.GetList() != nil {
// For Set-Cookie arrays, use Values() to get all cookies
getValue = Id(headersId).Dot("Values").Call(Lit(field.WireFormat))
} else {
// For other headers, use the standard approach
getValue = Id(headersId).Dot("Get").Call(Lit(field.WireFormat))
}

str, err := enc.FromString(
field.Type,
field.SrcName,
Id(headersId).Dot("Get").Call(Lit(field.WireFormat)),
getValue,
Id(headersId).Dot("Values").Call(Lit(field.WireFormat)),
true,
)
Expand Down
13 changes: 11 additions & 2 deletions pkg/clientgen/javascript.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,16 +523,25 @@ func (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string

for _, headerField := range respEnc.HeaderParameters {
isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie"
isSetCookieArray := isSetCookie && headerField.Type.GetList() != nil

if isSetCookie {
w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n")
w.WriteString("if (!BROWSER) {\n")
w = w.Indent()
}

js.seenHeaderResponse = true
fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat)

w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))
if isSetCookieArray {
// Handle multiple Set-Cookie headers
fieldValue := fmt.Sprintf("resp.headers.getAll(\"%s\")", headerField.WireFormat)
w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), fieldValue)
} else {
// Handle single value headers (including single Set-Cookie)
fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat)
w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))
}

if isSetCookie {
w = w.Dedent()
Expand Down
13 changes: 11 additions & 2 deletions pkg/clientgen/typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,16 +761,25 @@ func (ts *typescript) rpcCallSite(ns string, w *indentWriter, rpc *meta.RPC, rpc

for _, headerField := range respEnc.HeaderParameters {
isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie"
isSetCookieArray := isSetCookie && headerField.Type.GetList() != nil

if isSetCookie {
w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n")
w.WriteString("if (!BROWSER) {\n")
w = w.Indent()
}

ts.seenHeaderResponse = true
fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat)

w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), ts.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))
if isSetCookieArray {
// Handle multiple Set-Cookie headers
fieldValue := fmt.Sprintf("resp.headers.getAll(\"%s\")", headerField.WireFormat)
w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), fieldValue)
} else {
// Handle single value headers (including single Set-Cookie)
fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat)
w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), ts.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))
}

if isSetCookie {
w = w.Dedent()
Expand Down
32 changes: 27 additions & 5 deletions v2/codegen/apigen/endpointgen/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,13 @@ func (d *responseDesc) EncodeResponse() *Statement {
g.Line().Comment("Encode headers")
g.Id("headers").Op("=").Map(String()).Index().String().Values(DictFunc(func(dict Dict) {
for _, f := range resp.HeaderParameters {
if builtin, ok := f.Type.(schema.BuiltinType); ok {
encExpr := genutil.MarshalBuiltin(builtin.Kind, Id("resp").Dot(f.SrcName))
dict[Lit(f.WireName)] = Index().String().Values(encExpr)
} else {
d.gu.Errs.Addf(f.Type.ASTExpr().Pos(), "unsupported type in header: %s", d.gu.TypeToString(f.Type))
if unprocessedType := processHeaderField(f, dict); unprocessedType != nil {
d.gu.Errs.Addf(f.Type.ASTExpr().Pos(), "unsupported type in header: %s", d.gu.TypeToString(unprocessedType))
}
}
}))
}

})

// If response is a ptr we need to check it's not nil
Expand Down Expand Up @@ -149,6 +147,30 @@ func (d *responseDesc) EncodeResponse() *Statement {
})
}

// processHeaderField processes a single header field.
// Returns the unsupported schema. Type if processing fails, nil if successful.
func processHeaderField(f *apienc.ParameterEncoding, headersMap Dict) schema.Type {
kind, isList, ok := schemautil.IsBuiltinOrList(f.Type)
if !ok {
return f.Type
}

// Handling for Set-Cookie arrays
if f.WireName == "set-cookie" && isList && kind == schema.String {
headersMap[Lit(f.WireName)] = genutil.MarshalBuiltinList(kind, Id("resp").Dot(f.SrcName))
return nil
}

var encExpr Code
if isList {
encExpr = genutil.MarshalBuiltinList(kind, Id("resp").Dot(f.SrcName))
} else {
encExpr = genutil.MarshalBuiltin(kind, Id("resp").Dot(f.SrcName))
}
headersMap[Lit(f.WireName)] = Index().String().Values(encExpr)
return nil
}

func (d *responseDesc) DecodeExternalResp() *Statement {
if d.ep.Raw {
// TODO(andre) support
Expand Down
145 changes: 145 additions & 0 deletions v2/codegen/apigen/endpointgen/testdata/set_cookie_array.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
-- code.go --
package code

import "context"

type Response struct {
Cookies []string `header:"Set-Cookie"`
OtherHeader string `header:"X-Other"`
}

//encore:api public
func TestCookies(ctx context.Context) (*Response, error) { return nil, nil }

-- want:encore.gen.go --
// Code generated by encore. DO NOT EDIT.

package code

import "context"

// These functions are automatically generated and maintained by Encore
// to simplify calling them from other services, as they were implemented as methods.
// They are automatically updated by Encore whenever your API endpoints change.

// Interface defines the service's API surface area, primarily for mocking purposes.
//
// Raw endpoints are currently excluded from this interface, as Encore does not yet
// support service-to-service API calls to raw endpoints.
type Interface interface {
TestCookies(ctx context.Context) (*Response, error)
}
-- want:encore_internal__api.go --
package code

import (
"context"
__api "encore.dev/appruntime/apisdk/api"
__etype "encore.dev/appruntime/shared/etype"
jsoniter "github.com/json-iterator/go"
"net/http"
"net/url"
)

func init() {
__api.RegisterEndpoint(EncoreInternal_api_APIDesc_TestCookies, TestCookies)
}

type EncoreInternal_TestCookiesReq struct{}

type EncoreInternal_TestCookiesResp = *Response

var EncoreInternal_api_APIDesc_TestCookies = &__api.Desc[*EncoreInternal_TestCookiesReq, EncoreInternal_TestCookiesResp]{
Access: __api.Public,
AppHandler: func(ctx context.Context, reqData *EncoreInternal_TestCookiesReq) (EncoreInternal_TestCookiesResp, error) {
resp, err := TestCookies(ctx)
if err != nil {
return (*Response)(nil), err
}
return resp, nil
},
CloneReq: func(r *EncoreInternal_TestCookiesReq) (*EncoreInternal_TestCookiesReq, error) {
var clone *EncoreInternal_TestCookiesReq
bytes, err := jsoniter.ConfigDefault.Marshal(r)
if err == nil {
err = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)
}
return clone, err
},
CloneResp: func(r EncoreInternal_TestCookiesResp) (EncoreInternal_TestCookiesResp, error) {
var clone EncoreInternal_TestCookiesResp
bytes, err := jsoniter.ConfigDefault.Marshal(r)
if err == nil {
err = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)
}
return clone, err
},
DecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_TestCookiesResp, err error) {
resp = new(Response)
dec := new(__etype.Unmarshaller)
// Decode headers
h := httpResp.Header
resp.Cookies = __etype.UnmarshalList(dec, __etype.UnmarshalString, "set-cookie", h.Values("set-cookie"), false)
resp.OtherHeader = __etype.UnmarshalOne(dec, __etype.UnmarshalString, "x-other", h.Get("x-other"), false)

if err := dec.Error; err != nil {
return (*Response)(nil), err
}
return resp, nil
},
DecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_TestCookiesReq, pathParams __api.UnnamedParams, err error) {
reqData = new(EncoreInternal_TestCookiesReq)
return reqData, nil, nil
},
DefLoc: uint32(0x0),
EncodeExternalReq: func(reqData *EncoreInternal_TestCookiesReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {
return nil, nil, nil
},
EncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_TestCookiesResp, status int) (err error) {
respData := []byte{'\n'}
var headers map[string][]string
if resp != nil {

// Encode headers
headers = map[string][]string{
"set-cookie": __etype.MarshalList(__etype.MarshalString, resp.Cookies),
"x-other": []string{__etype.MarshalOne(__etype.MarshalString, resp.OtherHeader)},
}
}

// Set response headers
for k, vs := range headers {
for _, v := range vs {
w.Header().Add(k, v)
}
}

// Set HTTP status code
if status != 0 {
w.WriteHeader(status)
}

// Write response body
w.Write(respData)
return nil
},
Endpoint: "TestCookies",
Fallback: false,
GlobalMiddlewareIDs: []string{},
Methods: []string{"GET", "POST"},
Path: "/code.TestCookies",
PathParamNames: nil,
Raw: false,
RawHandler: nil,
RawPath: "/code.TestCookies",
ReqPath: func(reqData *EncoreInternal_TestCookiesReq) (string, __api.UnnamedParams, error) {
return "/code.TestCookies", nil, nil
},
ReqUserPayload: func(reqData *EncoreInternal_TestCookiesReq) any {
return nil
},
Service: "code",
ServiceMiddleware: []*__api.Middleware{},
SvcNum: 1,
Tags: nil,
}
11 changes: 11 additions & 0 deletions v2/parser/apis/api/apienc/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,17 @@ func describeParam(errs *perr.List, encodingHints *encodingHints, field schema.S
}

param.Location = location

// Validate Set-Cookie array types
if location == Header && strings.ToLower(param.WireName) == "set-cookie" {
if kind, isList, ok := schemautil.IsBuiltinOrList(param.Type); ok {
if isList && kind != schema.String {
errs.Addf(field.Type.ASTExpr().Pos(), "Set-Cookie header arrays must be []string, got []%s", kind)
return nil, false
}
}
}

return &param, true
}

Expand Down