Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

  • version: v2.4.0
  • server: echo
  • strict-server

Currently, OAS does not support situations where there are multiple set-cookie headers (OAI/OpenAPI-Specification#1237). This means that it is not possible to include multiple set-cookie headers in a Strict Server response (it is possible to have both set-cookie and Set-Cookie, but the fields are duplicated in Go).

There is no way to access echo.Context in Strict Server, so it is not possible to set cookies with it.
Is there a way to set multiple cookies in Strict Server?

(I looked for examples and tests, but I'm sorry if I missed anything!)

You must be logged in to vote

Replies: 3 comments · 2 replies

Comment options

@su8ru , having the same challenge, did you ever find a solution for this?

You must be logged in to vote
1 reply
@su8ru
Comment options

Hi, I haven't found the solution yet.

I needed to store OIDC state and nonce in cookies, so I unified them into one cookie.

Comment options

This is kindof hacky, but I think it will work for you.
So, you can overwrite the strict interface template in the config yaml as below so that it generates the SetCookie field as []string, and for-looping the response writer set cookie method.

This is what the generated code looks like

// output.go

type PostAuthLogin200ResponseHeaders struct {
	SetCookie []string
}


func (response PostAuthLogin200JSONResponse) VisitPostAuthLoginResponse(w http.ResponseWriter) error {
	w.Header().Set("Content-Type", "application/json")
	for _, cookie := range response.Headers.SetCookie {
		w.Header().Set("Set-Cookie", cookie)
	}
	w.WriteHeader(200)

	return json.NewEncoder(w).Encode(response.Body)
}

The config yaml

Basically, I added if eq .Name "Set-Cookie" to filter the header properties and apply the changes stated above so that it won't affect the behavior of other headers. The original template can be found at https://github.com/oapi-codegen/oapi-codegen/tree/main/pkg/codegen/templates/strict/strict-interface.tmpl. You might want to use the latest version and apply the same edit to it.

# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
package: api
output: gen.go
generate:
  echo-server: true
  models: true
  strict-server: true
  embedded-spec: true
output-options:
  user-templates:
    strict/strict-interface.tmpl: |
      {{range .}}
          {{$opid := .OperationId -}}
          type {{$opid | ucFirst}}RequestObject struct {
              {{range .PathParams -}}
                  {{.GoName | ucFirst}} {{.TypeDef}} {{.JsonTag}}
              {{end -}}
              {{if .RequiresParamObject -}}
                  Params {{$opid}}Params
              {{end -}}
              {{if .HasMaskedRequestContentTypes -}}
                  ContentType string
              {{end -}}
              {{$multipleBodies := gt (len .Bodies) 1 -}}
              {{range .Bodies -}}
                  {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}}
              {{end -}}
          }

          type {{$opid | ucFirst}}ResponseObject interface {
              Visit{{$opid}}Response(w http.ResponseWriter) error
          }

          {{range .Responses}}
              {{$statusCode := .StatusCode -}}
              {{$hasHeaders := ne 0 (len .Headers) -}}
              {{$fixedStatusCode := .HasFixedStatusCode -}}
              {{$isRef := .IsRef -}}
              {{$isExternalRef := .IsExternalRef -}}
              {{$ref := .Ref  | ucFirstWithPkgName -}}
              {{$headers := .Headers -}}

              {{if (and $hasHeaders (not $isRef)) -}}
                  type {{$opid}}{{$statusCode}}ResponseHeaders struct {
                      {{range .Headers -}}
                      {{/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! CUSTOM CODE BEGIN !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */}}
                          {{if eq .Name "Set-Cookie" -}}
                              SetCookie []string
                          {{else -}}
                              {{.GoName}} {{.Schema.TypeDecl}}
                          {{end -}}
                      {{/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  CUSTOM CODE END  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */}}
                      {{end -}}
                  }
              {{end}}

              {{range .Contents}}
                  {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}}
                  {{if and $fixedStatusCode $isRef -}}
                      {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (eq .NameTag "Multipart") -}}
                      type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response
                      {{else if $isExternalRef -}}
                      type {{$receiverTypeName}} struct { {{$ref}} }
                      {{else -}}
                      type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response }
                      {{end}}
                  {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}}
                      type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}}
                  {{else -}}
                      type {{$receiverTypeName}} struct {
                          Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}}
                          {{if $hasHeaders -}}
                              Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders
                          {{end -}}

                          {{if not $fixedStatusCode -}}
                              StatusCode int
                          {{end -}}

                          {{if not .HasFixedContentType -}}
                              ContentType string
                          {{end -}}

                          {{if not .IsSupported -}}
                              ContentLength int64
                          {{end -}}
                      }
                  {{end}}

                  func (response {{$receiverTypeName}}) Visit{{$opid}}Response(w http.ResponseWriter) error {
                                  {{if eq .NameTag "Multipart" -}}
                                      writer := multipart.NewWriter(w)
                                  {{end -}}
                                  w.Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType("{{.ContentType}}", map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}"{{.ContentType}}"{{else}}response.ContentType{{end}})
                                  {{if not .IsSupported -}}
                                      if response.ContentLength != 0 {
                                          w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength))
                                      }
                                  {{end -}}
                  {{/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! CUSTOM CODE BEGIN !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */}}
                                  {{range $headers -}}
                                      {{if eq .Name "Set-Cookie" -}}
                                          for _, cookie := range response.Headers.SetCookie {
                                              w.Header().Add("Set-Cookie", cookie)
                                          }
                                      {{else -}}
                                          w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}))
                                      {{end -}}
                                  {{end -}}
                  {{/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! CUSTOM CODE END !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */}}
                      w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}})
                      {{$hasBodyVar := or ($hasHeaders) (not $fixedStatusCode) (not .IsSupported)}}
                      {{if .IsJSON -}}
                          {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}}
                          return json.NewEncoder(w).Encode(response{{if $hasBodyVar}}.Body{{end}}{{if $hasUnionElements}}.union{{end}})
                      {{else if eq .NameTag "Text" -}}
                          _, err := w.Write([]byte({{if $hasBodyVar}}response.Body{{else}}response{{end}}))
                          return err
                      {{else if eq .NameTag "Formdata" -}}
                          if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil {
                              return err
                          } else {
                              _, err := w.Write([]byte(form.Encode()))
                              return err
                          }
                      {{else if eq .NameTag "Multipart" -}}
                          defer writer.Close()
                          return {{if $hasBodyVar}}response.Body{{else}}response{{end}}(writer);
                      {{else -}}
                          if closer, ok := response.Body.(io.ReadCloser); ok {
                              defer closer.Close()
                          }
                          _, err := io.Copy(w, response.Body)
                          return err
                      {{end}}{{/* if eq .NameTag "JSON" */ -}}
                  }
              {{end}}

              {{if eq 0 (len .Contents) -}}
                  {{if and $fixedStatusCode $isRef -}}
                      type {{$opid}}{{$statusCode}}Response {{if not $isExternalRef}}={{end}} {{$ref}}Response
                  {{else -}}
                      type {{$opid}}{{$statusCode}}Response struct {
                          {{if $hasHeaders -}}
                              Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders
                          {{end}}
                          {{if not $fixedStatusCode -}}
                              StatusCode int
                          {{end -}}
                      }
                  {{end -}}
                  func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(w http.ResponseWriter) error {
                      {{range $headers -}}
                      {{/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! CUSTOM CODE BEGIN !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */}}
                          {{if eq .Name "Set-Cookie" -}}
                              for _, cookie := range response.Headers.SetCookie {
                                  w.Header().Add("Set-Cookie", cookie)
                              }
                          {{else -}}
                              w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}))
                          {{end -}}
                      {{/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  CUSTOM CODE END  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */}}
                      {{end -}}
                      w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}})
                      return nil
                  }
              {{end}}
          {{end}}
      {{end}}

      // StrictServerInterface represents all server handlers.
      type StrictServerInterface interface {
      {{range .}}{{.SummaryAsComment }}
      // ({{.Method}} {{.Path}})
      {{$opid := .OperationId -}}
      {{$opid}}(ctx context.Context, request {{$opid | ucFirst}}RequestObject) ({{$opid | ucFirst}}ResponseObject, error)
      {{end}}{{/* range . */ -}}
      }

Alternative

You can of course overwrite the strict interface template to directly pass the echo context instead. But imo, the above solution feels better.

You must be logged in to vote
1 reply
@phoenisx
Comment options

This is far better solution. I'm new to golang, so was avoiding learning to write go templates, but it wasn't that hard.

This can save me from so many pain points like this one as well: #2085
I think I'll remove multiple error objects (only keep 2xx and 3xx objects) and everything else should be handled using the ErrorHandler by returning error from my hanlers.

Thanks @Juno-T for pointing this out.

Comment options

It would've been far easier to handler multiple set-cookies if we had access to ResponseWriter in our Strict handlers.

I was thinking to switch to non-strict version, but I realised that there's no validation for response bodies in non-strict handlers inside OapiRequestValidator. Still trying to figure out what works best here.

Providing custom templates is a better workaround, till this gets officially supported: #1802 (comment)

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
4 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.