diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-01-16 11:05:28 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-01-16 14:44:15 +0100 |
commit | f13531e608ac36cce9d679f6742a112cbab8afd1 (patch) | |
tree | c6cca6537b83f837e6fd22da559d2fb483799dc7 /resources | |
parent | b5d485060fc5f6222e730e5f59f7946b4602c7c6 (diff) |
Fix HEAD method in resources.GetRemote
Fixes #10604
Diffstat (limited to 'resources')
-rw-r--r-- | resources/resource_factories/create/integration_test.go | 56 | ||||
-rw-r--r-- | resources/resource_factories/create/remote.go | 102 |
2 files changed, 125 insertions, 33 deletions
diff --git a/resources/resource_factories/create/integration_test.go b/resources/resource_factories/create/integration_test.go new file mode 100644 index 000000000..e3a41d335 --- /dev/null +++ b/resources/resource_factories/create/integration_test.go @@ -0,0 +1,56 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package create_test + +import ( + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +func TestGetResourceHead(t *testing.T) { + + files := ` +-- config.toml -- +[security] + [security.http] + methods = ['(?i)GET|POST|HEAD'] + urls = ['.*gohugo\.io.*'] + +-- layouts/index.html -- +{{ $url := "https://gohugo.io/img/hugo.png" }} +{{ $opts := dict "method" "head" }} +{{ with resources.GetRemote $url $opts }} + {{ with .Err }} + {{ errorf "Unable to get remote resource: %s" . }} + {{ else }} + Head Content: {{ .Content }}. + {{ end }} +{{ else }} + {{ errorf "Unable to get remote resource: %s" $url }} +{{ end }} +` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ) + + b.Build() + + b.AssertFileContent("public/index.html", "Head Content: .") + +} diff --git a/resources/resource_factories/create/remote.go b/resources/resource_factories/create/remote.go index fa5533d7b..5216fff79 100644 --- a/resources/resource_factories/create/remote.go +++ b/resources/resource_factories/create/remote.go @@ -45,7 +45,29 @@ type HTTPError struct { Body string } -func toHTTPError(err error, res *http.Response) *HTTPError { +func responseToData(res *http.Response, readBody bool) map[string]any { + var body []byte + if readBody { + body, _ = ioutil.ReadAll(res.Body) + } + + m := map[string]any{ + "StatusCode": res.StatusCode, + "Status": res.Status, + "TransferEncoding": res.TransferEncoding, + "ContentLength": res.ContentLength, + "ContentType": res.Header.Get("Content-Type"), + } + + if readBody { + m["Body"] = string(body) + } + + return m + +} + +func toHTTPError(err error, res *http.Response, readBody bool) *HTTPError { if err == nil { panic("err is nil") } @@ -56,19 +78,9 @@ func toHTTPError(err error, res *http.Response) *HTTPError { } } - var body []byte - body, _ = ioutil.ReadAll(res.Body) - return &HTTPError{ error: err, - Data: map[string]any{ - "StatusCode": res.StatusCode, - "Status": res.Status, - "Body": string(body), - "TransferEncoding": res.TransferEncoding, - "ContentLength": res.ContentLength, - "ContentType": res.Header.Get("Content-Type"), - }, + Data: responseToData(res, readBody), } } @@ -80,6 +92,12 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou return nil, fmt.Errorf("failed to parse URL for resource %s: %w", uri, err) } + method := "GET" + if s, ok := maps.LookupEqualFold(optionsm, "method"); ok { + method = strings.ToUpper(s.(string)) + } + isHeadMethod := method == "HEAD" + resourceID := calculateResourceID(uri, optionsm) _, httpResponse, err := c.cacheGetResource.GetOrCreate(resourceID, func() (io.ReadCloser, error) { @@ -100,15 +118,16 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou if err != nil { return nil, err } + defer res.Body.Close() httpResponse, err := httputil.DumpResponse(res, true) if err != nil { - return nil, toHTTPError(err, res) + return nil, toHTTPError(err, res, !isHeadMethod) } if res.StatusCode != http.StatusNotFound { if res.StatusCode < 200 || res.StatusCode > 299 { - return nil, toHTTPError(fmt.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res) + return nil, toHTTPError(fmt.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res, !isHeadMethod) } } @@ -124,15 +143,24 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou if err != nil { return nil, err } + defer res.Body.Close() if res.StatusCode == http.StatusNotFound { // Not found. This matches how looksup for local resources work. return nil, nil } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read remote resource %q: %w", uri, err) + var ( + body []byte + mediaType media.Type + ) + // A response to a HEAD method should not have a body. If it has one anyway, that body must be ignored. + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD + if !isHeadMethod && res.Body != nil { + body, err = ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read remote resource %q: %w", uri, err) + } } filename := path.Base(rURL.Path) @@ -142,30 +170,38 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou } } - var extensionHints []string - contentType := res.Header.Get("Content-Type") - // mime.ExtensionsByType gives a long list of extensions for text/plain, - // just use ".txt". - if strings.HasPrefix(contentType, "text/plain") { - extensionHints = []string{".txt"} + if isHeadMethod { + // We have no body to work with, so we need to use the Content-Type header. + mediaType, _ = media.FromString(contentType) } else { - exts, _ := mime.ExtensionsByType(contentType) - if exts != nil { - extensionHints = exts + + var extensionHints []string + + // mime.ExtensionsByType gives a long list of extensions for text/plain, + // just use ".txt". + if strings.HasPrefix(contentType, "text/plain") { + extensionHints = []string{".txt"} + } else { + exts, _ := mime.ExtensionsByType(contentType) + if exts != nil { + extensionHints = exts + } } - } - // Look for a file extension. If it's .txt, look for a more specific. - if extensionHints == nil || extensionHints[0] == ".txt" { - if ext := path.Ext(filename); ext != "" { - extensionHints = []string{ext} + // Look for a file extension. If it's .txt, look for a more specific. + if extensionHints == nil || extensionHints[0] == ".txt" { + if ext := path.Ext(filename); ext != "" { + extensionHints = []string{ext} + } } + + // Now resolve the media type primarily using the content. + mediaType = media.FromContent(c.rs.MediaTypes, extensionHints, body) + } - // Now resolve the media type primarily using the content. - mediaType := media.FromContent(c.rs.MediaTypes, extensionHints, body) if mediaType.IsZero() { return nil, fmt.Errorf("failed to resolve media type for remote resource %q", uri) } |