Update go-restful

pull/6/head
Brendan Burns 2015-08-26 21:20:22 -07:00
parent b6f2f396ba
commit 535c509dd3
12 changed files with 268 additions and 49 deletions

4
Godeps/Godeps.json generated
View File

@ -215,8 +215,8 @@
},
{
"ImportPath": "github.com/emicklei/go-restful",
"Comment": "v1.1.3-76-gbfd6ff2",
"Rev": "bfd6ff29d2961031cec64346a92bae4cde96c868"
"Comment": "v1.1.3-98-g1f9a0ee",
"Rev": "1f9a0ee00ff93717a275e15b30cf7df356255877"
},
{
"ImportPath": "github.com/evanphx/json-patch",

View File

@ -1,5 +1,10 @@
Change history of go-restful
=
2015-08-06
- add support for reading entities from compressed request content
- use sync.Pool for compressors of http response and request body
- add Description to Parameter for documentation in Swagger UI
2015-03-20
- add configurable logging

View File

@ -47,7 +47,7 @@ func (u UserResource) findUser(request *restful.Request, response *restful.Respo
- Filters for intercepting the request → response flow on Service or Route level
- Request-scoped variables using attributes
- Containers for WebServices on different HTTP endpoints
- Content encoding (gzip,deflate) of responses
- Content encoding (gzip,deflate) of request and response payloads
- Automatic responses on OPTIONS (using a filter)
- Automatic CORS request handling (using a filter)
- API declaration for Swagger UI (see swagger package)

View File

@ -73,15 +73,13 @@ func NewCompressingResponseWriter(httpWriter http.ResponseWriter, encoding strin
c.writer = httpWriter
var err error
if ENCODING_GZIP == encoding {
c.compressor, err = gzip.NewWriterLevel(httpWriter, gzip.BestSpeed)
if err != nil {
return nil, err
}
w := GzipWriterPool.Get().(*gzip.Writer)
w.Reset(httpWriter)
c.compressor = w
} else if ENCODING_DEFLATE == encoding {
c.compressor, err = zlib.NewWriterLevel(httpWriter, zlib.BestSpeed)
if err != nil {
return nil, err
}
w := ZlibWriterPool.Get().(*zlib.Writer)
w.Reset(httpWriter)
c.compressor = w
} else {
return nil, errors.New("Unknown encoding:" + encoding)
}

View File

@ -1,11 +1,17 @@
package restful
import (
"bytes"
"compress/gzip"
"compress/zlib"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
// go test -v -test.run TestGzip ...restful
func TestGzip(t *testing.T) {
EnableContentEncoding = true
httpRequest, _ := http.NewRequest("GET", "/test", nil)
@ -27,6 +33,17 @@ func TestGzip(t *testing.T) {
if httpWriter.Header().Get("Content-Encoding") != "gzip" {
t.Fatal("Missing gzip header")
}
reader, err := gzip.NewReader(httpWriter.Body)
if err != nil {
t.Fatal(err.Error())
}
data, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err.Error())
}
if got, want := string(data), "Hello World"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestDeflate(t *testing.T) {
@ -50,4 +67,61 @@ func TestDeflate(t *testing.T) {
if httpWriter.Header().Get("Content-Encoding") != "deflate" {
t.Fatal("Missing deflate header")
}
reader, err := zlib.NewReader(httpWriter.Body)
if err != nil {
t.Fatal(err.Error())
}
data, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err.Error())
}
if got, want := string(data), "Hello World"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestGzipDecompressRequestBody(t *testing.T) {
b := new(bytes.Buffer)
w := newGzipWriter()
w.Reset(b)
io.WriteString(w, `{"msg":"hi"}`)
w.Flush()
w.Close()
req := new(Request)
httpRequest, _ := http.NewRequest("GET", "/", bytes.NewReader(b.Bytes()))
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Content-Encoding", "gzip")
req.Request = httpRequest
doCacheReadEntityBytes = false
doc := make(map[string]interface{})
req.ReadEntity(&doc)
if got, want := doc["msg"], "hi"; got != want {
t.Errorf("got %v want %v", got, want)
}
}
func TestZlibDecompressRequestBody(t *testing.T) {
b := new(bytes.Buffer)
w := newZlibWriter()
w.Reset(b)
io.WriteString(w, `{"msg":"hi"}`)
w.Flush()
w.Close()
req := new(Request)
httpRequest, _ := http.NewRequest("GET", "/", bytes.NewReader(b.Bytes()))
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Content-Encoding", "deflate")
req.Request = httpRequest
doCacheReadEntityBytes = false
doc := make(map[string]interface{})
req.ReadEntity(&doc)
if got, want := doc["msg"], "hi"; got != want {
t.Errorf("got %v want %v", got, want)
}
}

View File

@ -0,0 +1,63 @@
package restful
import (
"bytes"
"compress/gzip"
"compress/zlib"
"sync"
)
// GzipWriterPool is used to get reusable zippers.
// The Get() result must be type asserted to *gzip.Writer.
var GzipWriterPool = &sync.Pool{
New: func() interface{} {
return newGzipWriter()
},
}
func newGzipWriter() *gzip.Writer {
// create with an empty bytes writer; it will be replaced before using the gzipWriter
writer, err := gzip.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
if err != nil {
panic(err.Error())
}
return writer
}
// GzipReaderPool is used to get reusable zippers.
// The Get() result must be type asserted to *gzip.Reader.
var GzipReaderPool = &sync.Pool{
New: func() interface{} {
return newGzipReader()
},
}
func newGzipReader() *gzip.Reader {
// create with an empty reader (but with GZIP header); it will be replaced before using the gzipReader
w := GzipWriterPool.Get().(*gzip.Writer)
b := new(bytes.Buffer)
w.Reset(b)
w.Flush()
w.Close()
reader, err := gzip.NewReader(bytes.NewReader(b.Bytes()))
if err != nil {
panic(err.Error())
}
return reader
}
// ZlibWriterPool is used to get reusable zippers.
// The Get() result must be type asserted to *zlib.Writer.
var ZlibWriterPool = &sync.Pool{
New: func() interface{} {
return newZlibWriter()
},
}
func newZlibWriter() *zlib.Writer {
writer, err := zlib.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
if err != nil {
panic(err.Error())
}
return writer
}

View File

@ -11,6 +11,7 @@ import (
"os"
"runtime"
"strings"
"sync"
"github.com/emicklei/go-restful/log"
)
@ -18,6 +19,7 @@ import (
// Container holds a collection of WebServices and a http.ServeMux to dispatch http requests.
// The requests are further dispatched to routes of WebServices using a RouteSelector
type Container struct {
webServicesLock sync.RWMutex
webServices []*WebService
ServeMux *http.ServeMux
isRegisteredOnRoot bool
@ -83,6 +85,8 @@ func (c *Container) EnableContentEncoding(enabled bool) {
// Add a WebService to the Container. It will detect duplicate root paths and panic in that case.
func (c *Container) Add(service *WebService) *Container {
c.webServicesLock.Lock()
defer c.webServicesLock.Unlock()
// If registered on root then no additional specific mapping is needed
if !c.isRegisteredOnRoot {
pattern := c.fixedPrefixPath(service.RootPath())
@ -122,6 +126,19 @@ func (c *Container) Add(service *WebService) *Container {
return c
}
func (c *Container) Remove(ws *WebService) error {
c.webServicesLock.Lock()
defer c.webServicesLock.Unlock()
newServices := []*WebService{}
for ix := range c.webServices {
if c.webServices[ix].rootPath != ws.rootPath {
newServices = append(newServices, c.webServices[ix])
}
}
c.webServices = newServices
return nil
}
// logStackOnRecover is the default RecoverHandleFunction and is called
// when DoNotRecover is false and the recoverHandleFunc is not set for the container.
// Default implementation logs the stacktrace and writes the stacktrace on the response.
@ -190,9 +207,16 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
}
}
// Find best match Route ; err is non nil if no match was found
webService, route, err := c.router.SelectRoute(
c.webServices,
httpRequest)
var webService *WebService
var route *Route
var err error
func() {
c.webServicesLock.RLock()
defer c.webServicesLock.RUnlock()
webService, route, err = c.router.SelectRoute(
c.webServices,
httpRequest)
}()
if err != nil {
// a non-200 response has already been written
// run container filters anyway ; they should not touch the response...
@ -272,7 +296,13 @@ func (c *Container) Filter(filter FilterFunction) {
// RegisteredWebServices returns the collections of added WebServices
func (c Container) RegisteredWebServices() []*WebService {
return c.webServices
c.webServicesLock.RLock()
defer c.webServicesLock.RUnlock()
result := make([]*WebService, len(c.webServices))
for ix := range c.webServices {
result[ix] = c.webServices[ix]
}
return result
}
// computeAllowedMethods returns a list of HTTP methods that are valid for a Request

View File

@ -95,8 +95,14 @@ func (p *Parameter) DataType(typeName string) *Parameter {
return p
}
// DefaultValue sets the default value field and returnw the receiver
// DefaultValue sets the default value field and returns the receiver
func (p *Parameter) DefaultValue(stringRepresentation string) *Parameter {
p.data.DefaultValue = stringRepresentation
return p
}
// Description sets the description value field and returns the receiver
func (p *Parameter) Description(doc string) *Parameter {
p.data.Description = doc
return p
}

View File

@ -6,6 +6,8 @@ package restful
import (
"bytes"
"compress/gzip"
"compress/zlib"
"encoding/json"
"encoding/xml"
"io"
@ -82,15 +84,17 @@ func (r *Request) HeaderParameter(name string) string {
// ReadEntity checks the Accept header and reads the content into the entityPointer
// May be called multiple times in the request-response flow
func (r *Request) ReadEntity(entityPointer interface{}) (err error) {
defer r.Request.Body.Close()
contentType := r.Request.Header.Get(HEADER_ContentType)
contentEncoding := r.Request.Header.Get(HEADER_ContentEncoding)
if doCacheReadEntityBytes {
return r.cachingReadEntity(contentType, entityPointer)
return r.cachingReadEntity(contentType, contentEncoding, entityPointer)
}
// unmarshall directly from request Body
return r.decodeEntity(r.Request.Body, contentType, entityPointer)
return r.decodeEntity(r.Request.Body, contentType, contentEncoding, entityPointer)
}
func (r *Request) cachingReadEntity(contentType string, entityPointer interface{}) (err error) {
func (r *Request) cachingReadEntity(contentType string, contentEncoding string, entityPointer interface{}) (err error) {
var buffer []byte
if r.bodyContent != nil {
buffer = *r.bodyContent
@ -101,22 +105,38 @@ func (r *Request) cachingReadEntity(contentType string, entityPointer interface{
}
r.bodyContent = &buffer
}
return r.decodeEntity(bytes.NewReader(buffer), contentType, entityPointer)
return r.decodeEntity(bytes.NewReader(buffer), contentType, contentEncoding, entityPointer)
}
func (r *Request) decodeEntity(reader io.Reader, contentType string, entityPointer interface{}) (err error) {
if strings.Contains(contentType, MIME_XML) {
return xml.NewDecoder(reader).Decode(entityPointer)
func (r *Request) decodeEntity(reader io.Reader, contentType string, contentEncoding string, entityPointer interface{}) (err error) {
entityReader := reader
// check if the request body needs decompression
if ENCODING_GZIP == contentEncoding {
gzipReader := GzipReaderPool.Get().(*gzip.Reader)
gzipReader.Reset(reader)
entityReader = gzipReader
} else if ENCODING_DEFLATE == contentEncoding {
zlibReader, err := zlib.NewReader(reader)
if err != nil {
return err
}
entityReader = zlibReader
}
// decode JSON
if strings.Contains(contentType, MIME_JSON) || MIME_JSON == defaultRequestContentType {
decoder := json.NewDecoder(reader)
decoder := json.NewDecoder(entityReader)
decoder.UseNumber()
return decoder.Decode(entityPointer)
}
if MIME_XML == defaultRequestContentType {
return xml.NewDecoder(reader).Decode(entityPointer)
// decode XML
if strings.Contains(contentType, MIME_XML) || MIME_XML == defaultRequestContentType {
return xml.NewDecoder(entityReader).Decode(entityPointer)
}
return NewError(400, "Unable to unmarshal content of type:"+contentType)
return NewError(http.StatusBadRequest, "Unable to unmarshal content of type:"+contentType)
}
// SetAttribute adds or replaces the attribute with the given value.

View File

@ -28,11 +28,12 @@ type Response struct {
statusCode int // HTTP status code that has been written explicity (if zero then net/http has written 200)
contentLength int // number of bytes written for the response body
prettyPrint bool // controls the indentation feature of XML and JSON serialization. It is initialized using var PrettyPrintResponses.
err error // err property is kept when WriteError is called
}
// Creates a new response based on a http ResponseWriter.
func NewResponse(httpWriter http.ResponseWriter) *Response {
return &Response{httpWriter, "", []string{}, http.StatusOK, 0, PrettyPrintResponses} // empty content-types
return &Response{httpWriter, "", []string{}, http.StatusOK, 0, PrettyPrintResponses, nil} // empty content-types
}
// If Accept header matching fails, fall back to this type, otherwise
@ -182,6 +183,7 @@ func (r *Response) WriteJson(value interface{}, contentType string) error {
// WriteError write the http status and the error string on the response.
func (r *Response) WriteError(httpStatus int, err error) error {
r.err = err
return r.WriteErrorString(httpStatus, err.Error())
}
@ -203,21 +205,30 @@ func (r *Response) WriteErrorString(status int, errorReason string) error {
// WriteHeader is overridden to remember the Status Code that has been written.
// Note that using this method, the status value is only written when
// - calling WriteEntity,
// - or directly calling WriteAsXml or WriteAsJson,
// - or if the status is one for which no response is allowed (i.e.,
// 204 (http.StatusNoContent) or 304 (http.StatusNotModified))
// calling WriteEntity,
// or directly calling WriteAsXml or WriteAsJson,
// or if the status is one for which no response is allowed:
//
// 202 = http.StatusAccepted
// 204 = http.StatusNoContent
// 206 = http.StatusPartialContent
// 304 = http.StatusNotModified
//
// If this behavior does not fit your need then you can write to the underlying response, such as:
// response.ResponseWriter.WriteHeader(http.StatusAccepted)
func (r *Response) WriteHeader(httpStatus int) {
r.statusCode = httpStatus
// if 201,204,304 then WriteEntity will not be called so we need to pass this code
// if 202,204,206,304 then WriteEntity will not be called so we need to pass this code
if http.StatusNoContent == httpStatus ||
http.StatusNotModified == httpStatus ||
http.StatusPartialContent == httpStatus {
http.StatusPartialContent == httpStatus ||
http.StatusAccepted == httpStatus {
r.ResponseWriter.WriteHeader(httpStatus)
}
}
// StatusCode returns the code that has been written using WriteHeader.
// If WriteHeader, WriteEntity or WriteAsXml has not been called (yet) then return 200 OK.
func (r Response) StatusCode() int {
if 0 == r.statusCode {
// no status code has been written yet; assume OK
@ -245,3 +256,8 @@ func (r Response) ContentLength() int {
func (r Response) CloseNotify() <-chan bool {
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Error returns the err created by WriteError
func (r Response) Error() error {
return r.err
}

View File

@ -9,7 +9,7 @@ import (
func TestWriteHeader(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true, nil}
resp.WriteHeader(123)
if resp.StatusCode() != 123 {
t.Errorf("Unexpected status code:%d", resp.StatusCode())
@ -18,7 +18,7 @@ func TestWriteHeader(t *testing.T) {
func TestNoWriteHeader(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true, nil}
if resp.StatusCode() != http.StatusOK {
t.Errorf("Unexpected status code:%d", resp.StatusCode())
}
@ -31,7 +31,7 @@ type food struct {
// go test -v -test.run TestMeasureContentLengthXml ...restful
func TestMeasureContentLengthXml(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true, nil}
resp.WriteAsXml(food{"apple"})
if resp.ContentLength() != 76 {
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
@ -41,7 +41,7 @@ func TestMeasureContentLengthXml(t *testing.T) {
// go test -v -test.run TestMeasureContentLengthJson ...restful
func TestMeasureContentLengthJson(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true, nil}
resp.WriteAsJson(food{"apple"})
if resp.ContentLength() != 22 {
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
@ -51,7 +51,7 @@ func TestMeasureContentLengthJson(t *testing.T) {
// go test -v -test.run TestMeasureContentLengthJsonNotPretty ...restful
func TestMeasureContentLengthJsonNotPretty(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, false}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, false, nil}
resp.WriteAsJson(food{"apple"})
if resp.ContentLength() != 16 {
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
@ -61,7 +61,7 @@ func TestMeasureContentLengthJsonNotPretty(t *testing.T) {
// go test -v -test.run TestMeasureContentLengthWriteErrorString ...restful
func TestMeasureContentLengthWriteErrorString(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true, nil}
resp.WriteErrorString(404, "Invalid")
if resp.ContentLength() != len("Invalid") {
t.Errorf("Incorrect measured length:%d", resp.ContentLength())
@ -79,7 +79,7 @@ func TestStatusIsPassedToResponse(t *testing.T) {
{write: 400, read: 200},
} {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true}
resp := Response{httpWriter, "*/*", []string{"*/*"}, 0, 0, true, nil}
resp.WriteHeader(each.write)
if got, want := httpWriter.Code, each.read; got != want {
t.Error("got %v want %v", got, want)
@ -90,7 +90,7 @@ func TestStatusIsPassedToResponse(t *testing.T) {
// go test -v -test.run TestStatusCreatedAndContentTypeJson_Issue54 ...restful
func TestStatusCreatedAndContentTypeJson_Issue54(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0, true}
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0, true, nil}
resp.WriteHeader(201)
resp.WriteAsJson(food{"Juicy"})
if httpWriter.HeaderMap.Get("Content-Type") != "application/json" {
@ -112,7 +112,7 @@ func (e errorOnWriteRecorder) Write(bytes []byte) (int, error) {
// go test -v -test.run TestLastWriteErrorCaught ...restful
func TestLastWriteErrorCaught(t *testing.T) {
httpWriter := errorOnWriteRecorder{httptest.NewRecorder()}
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0, true}
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0, true, nil}
err := resp.WriteAsJson(food{"Juicy"})
if err.Error() != "fail" {
t.Errorf("Unexpected error message:%v", err)
@ -123,7 +123,7 @@ func TestLastWriteErrorCaught(t *testing.T) {
func TestAcceptStarStar_Issue83(t *testing.T) {
httpWriter := httptest.NewRecorder()
// Accept Produces
resp := Response{httpWriter, "application/bogus,*/*;q=0.8", []string{"application/json"}, 0, 0, true}
resp := Response{httpWriter, "application/bogus,*/*;q=0.8", []string{"application/json"}, 0, 0, true, nil}
resp.WriteEntity(food{"Juicy"})
ct := httpWriter.Header().Get("Content-Type")
if "application/json" != ct {
@ -135,7 +135,7 @@ func TestAcceptStarStar_Issue83(t *testing.T) {
func TestAcceptSkipStarStar_Issue83(t *testing.T) {
httpWriter := httptest.NewRecorder()
// Accept Produces
resp := Response{httpWriter, " application/xml ,*/* ; q=0.8", []string{"application/json", "application/xml"}, 0, 0, true}
resp := Response{httpWriter, " application/xml ,*/* ; q=0.8", []string{"application/json", "application/xml"}, 0, 0, true, nil}
resp.WriteEntity(food{"Juicy"})
ct := httpWriter.Header().Get("Content-Type")
if "application/xml" != ct {
@ -147,7 +147,7 @@ func TestAcceptSkipStarStar_Issue83(t *testing.T) {
func TestAcceptXmlBeforeStarStar_Issue83(t *testing.T) {
httpWriter := httptest.NewRecorder()
// Accept Produces
resp := Response{httpWriter, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", []string{"application/json"}, 0, 0, true}
resp := Response{httpWriter, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", []string{"application/json"}, 0, 0, true, nil}
resp.WriteEntity(food{"Juicy"})
ct := httpWriter.Header().Get("Content-Type")
if "application/json" != ct {
@ -158,7 +158,7 @@ func TestAcceptXmlBeforeStarStar_Issue83(t *testing.T) {
// go test -v -test.run TestWriteHeaderNoContent_Issue124 ...restful
func TestWriteHeaderNoContent_Issue124(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "text/plain", []string{"text/plain"}, 0, 0, true}
resp := Response{httpWriter, "text/plain", []string{"text/plain"}, 0, 0, true, nil}
resp.WriteHeader(http.StatusNoContent)
if httpWriter.Code != http.StatusNoContent {
t.Errorf("got %d want %d", httpWriter.Code, http.StatusNoContent)
@ -168,7 +168,7 @@ func TestWriteHeaderNoContent_Issue124(t *testing.T) {
// go test -v -test.run TestStatusCreatedAndContentTypeJson_Issue163 ...restful
func TestStatusCreatedAndContentTypeJson_Issue163(t *testing.T) {
httpWriter := httptest.NewRecorder()
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0, true}
resp := Response{httpWriter, "application/json", []string{"application/json"}, 0, 0, true, nil}
resp.WriteHeader(http.StatusNotModified)
if httpWriter.Code != http.StatusNotModified {
t.Errorf("Got %d want %d", httpWriter.Code, http.StatusNotModified)

View File

@ -173,7 +173,14 @@ func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Re
} else {
host = hostvalues[0]
}
(&decl).BasePath = fmt.Sprintf("http://%s", host)
// inspect Referer for the scheme (http vs https)
scheme := "http"
if referer := req.Request.Header["Referer"]; len(referer) > 0 {
if strings.HasPrefix(referer[0], "https") {
scheme = "https"
}
}
(&decl).BasePath = fmt.Sprintf("%s://%s", scheme, host)
}
resp.WriteAsJson(decl)
}