summaryrefslogtreecommitdiffstats
path: root/resources
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2023-01-16 11:05:28 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2023-01-16 14:44:15 +0100
commitf13531e608ac36cce9d679f6742a112cbab8afd1 (patch)
treec6cca6537b83f837e6fd22da559d2fb483799dc7 /resources
parentb5d485060fc5f6222e730e5f59f7946b4602c7c6 (diff)
Fix HEAD method in resources.GetRemote
Fixes #10604
Diffstat (limited to 'resources')
-rw-r--r--resources/resource_factories/create/integration_test.go56
-rw-r--r--resources/resource_factories/create/remote.go102
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)
}