Compare commits

...

2 Commits

Author SHA1 Message Date
Frédéric Guillot 8d8f78241d Add native lazy loading for images and iframes 5 years ago
Peter De Wachter 937492f6f5 Do not buffer responses in the image proxy 5 years ago
  1. 5
      http/response/builder.go
  2. 4
      reader/sanitizer/sanitizer.go
  3. 20
      reader/sanitizer/sanitizer_test.go
  4. 2
      template/html/bookmark_entries.html
  5. 2
      template/html/category_entries.html
  6. 4
      template/html/entry.html
  7. 2
      template/html/feed_entries.html
  8. 2
      template/html/feeds.html
  9. 2
      template/html/history_entries.html
  10. 2
      template/html/search_entries.html
  11. 2
      template/html/unread_entries.html
  12. 34
      template/views.go
  13. 29
      ui/proxy.go

@ -8,6 +8,7 @@ import (
"compress/flate"
"compress/gzip"
"fmt"
"io"
"net/http"
"strings"
"time"
@ -84,6 +85,10 @@ func (b *Builder) Write() {
b.compress([]byte(v))
case error:
b.compress([]byte(v.Error()))
case io.Reader:
// Compression not implemented in this case
b.writeHeaders()
io.Copy(b.w, v)
}
}

@ -137,7 +137,9 @@ func getExtraAttributes(tagName string) ([]string, []string) {
case "video", "audio":
return []string{"controls"}, []string{"controls"}
case "iframe":
return []string{"sandbox"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups"`}
return []string{"sandbox", "loading"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups"`, `loading="lazy"`}
case "img":
return []string{"loading"}, []string{`loading="lazy"`}
default:
return nil, nil
}

@ -7,7 +7,7 @@ package sanitizer // import "miniflux.app/reader/sanitizer"
import "testing"
func TestValidInput(t *testing.T) {
input := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test">.</p>`
input := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>`
output := Sanitize("http://example.org/", input)
if input != output {
@ -16,7 +16,7 @@ func TestValidInput(t *testing.T) {
}
func TestSelfClosingTags(t *testing.T) {
input := `<p>This <br> is a <strong>text</strong> <br/>with an image: <img src="http://example.org/" alt="Test"/>.</p>`
input := `<p>This <br> is a <strong>text</strong> <br/>with an image: <img src="http://example.org/" alt="Test" loading="lazy"/>.</p>`
output := Sanitize("http://example.org/", input)
if input != output {
@ -35,7 +35,7 @@ func TestTable(t *testing.T) {
func TestRelativeURL(t *testing.T) {
input := `This <a href="/test.html">link is relative</a> and this image: <img src="../folder/image.png"/>`
expected := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png"/>`
expected := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy"/>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -165,7 +165,7 @@ func TestEspaceAttributes(t *testing.T) {
func TestReplaceYoutubeURL(t *testing.T) {
input := `<iframe src="http://www.youtube.com/embed/test123?version=3&#038;rel=1&#038;fs=1&#038;autohide=2&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;wmode=transparent"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -175,7 +175,7 @@ func TestReplaceYoutubeURL(t *testing.T) {
func TestReplaceSecureYoutubeURL(t *testing.T) {
input := `<iframe src="https://www.youtube.com/embed/test123"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -185,7 +185,7 @@ func TestReplaceSecureYoutubeURL(t *testing.T) {
func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
input := `<iframe src="https://www.youtube.com/embed/test123?rel=0&amp;controls=0"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;controls=0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;controls=0" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -195,7 +195,7 @@ func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
input := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;controls=0" sandbox="allow-scripts allow-same-origin"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;controls=0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;controls=0" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -205,7 +205,7 @@ func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) {
input := `<iframe src="//www.youtube.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -215,7 +215,7 @@ func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) {
func TestReplaceIframeURL(t *testing.T) {
input := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0"></iframe>`
expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`
expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
output := Sanitize("http://example.org/", input)
if expected != output {
@ -224,7 +224,7 @@ func TestReplaceIframeURL(t *testing.T) {
}
func TestReplaceNoScript(t *testing.T) {
input := `<p>Before paragraph.</p><noscript>Inside <code>noscript</code> tag with an image: <img src="http://example.org/" alt="Test"></noscript><p>After paragraph.</p>`
input := `<p>Before paragraph.</p><noscript>Inside <code>noscript</code> tag with an image: <img src="http://example.org/" alt="Test" loading="lazy"></noscript><p>After paragraph.</p>`
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
output := Sanitize("http://example.org/", input)

@ -14,7 +14,7 @@
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
</span>

@ -36,7 +36,7 @@
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "categoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}">{{ .Title }}</a>
</span>

@ -57,7 +57,7 @@
<div class="entry-meta">
<span class="entry-website">
{{ if ne .entry.Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" alt="{{ .entry.Feed.Title }}">
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
{{ end }}
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
</span>
@ -105,7 +105,7 @@
</div>
{{ else if hasPrefix .MimeType "image/" }}
<div class="enclosure-image">
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" alt="{{ .URL }} ({{ .MimeType }})">
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
</div>
{{ end }}

@ -64,7 +64,7 @@
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "feedEntry" "feedID" .Feed.ID "entryID" .ID }}">{{ .Title }}</a>
</span>

@ -28,7 +28,7 @@
<div class="item-header">
<span class="item-title">
{{ if .Icon }}
<img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16" alt="{{ .Title }}">
<img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Title }}">
{{ end }}
{{ if .Disabled }} 🚫 {{ end }}
<a href="{{ route "feedEntries" "feedID" .ID }}">{{ .Title }}</a>

@ -27,7 +27,7 @@
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "readEntry" "entryID" .ID }}">{{ .Title }}</a>
</span>

@ -14,7 +14,7 @@
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "searchEntry" "entryID" .ID }}?q={{ $.searchQuery }}">{{ .Title }}</a>
</span>

@ -36,7 +36,7 @@
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "unreadEntry" "entryID" .ID }}">{{ .Title }}</a>
</span>

@ -129,7 +129,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
</span>
@ -239,7 +239,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "categoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}">{{ .Title }}</a>
</span>
@ -636,7 +636,7 @@ var templateViewsMap = map[string]string{
<div class="entry-meta">
<span class="entry-website">
{{ if ne .entry.Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" alt="{{ .entry.Feed.Title }}">
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
{{ end }}
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
</span>
@ -684,7 +684,7 @@ var templateViewsMap = map[string]string{
</div>
{{ else if hasPrefix .MimeType "image/" }}
<div class="enclosure-image">
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" alt="{{ .URL }} ({{ .MimeType }})">
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
</div>
{{ end }}
@ -769,7 +769,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "feedEntry" "feedID" .Feed.ID "entryID" .ID }}">{{ .Title }}</a>
</span>
@ -829,7 +829,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if .Icon }}
<img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16" alt="{{ .Title }}">
<img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Title }}">
{{ end }}
{{ if .Disabled }} 🚫 {{ end }}
<a href="{{ route "feedEntries" "feedID" .ID }}">{{ .Title }}</a>
@ -907,7 +907,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "readEntry" "entryID" .ID }}">{{ .Title }}</a>
</span>
@ -1175,7 +1175,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "searchEntry" "entryID" .ID }}?q={{ $.searchQuery }}">{{ .Title }}</a>
</span>
@ -1370,7 +1370,7 @@ var templateViewsMap = map[string]string{
<div class="item-header">
<span class="item-title">
{{ if ne .Feed.Icon.IconID 0 }}
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" alt="{{ .Feed.Title }}">
<img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
{{ end }}
<a href="{{ route "unreadEntry" "entryID" .ID }}">{{ .Title }}</a>
</span>
@ -1467,25 +1467,25 @@ var templateViewsMap = map[string]string{
var templateViewsMapChecksums = map[string]string{
"about": "844e3313c33ae31a74b904f6ef5d60299773620d8450da6f760f9f317217c51e",
"add_subscription": "a0f1d2bc02b6adc83dbeae593f74d9b936102cd6dd73302cdbec2137cafdcdd9",
"bookmark_entries": "609f4b2342152fe495a219a32f17a4528b01807d61f53cee0cbebf728be73c42",
"bookmark_entries": "65588da78665699dd3f287f68325e9777d511f1a57fee4131a5bb6d00bb68df8",
"categories": "642ee3cddbd825ee6ab5a77caa0d371096b55de0f1bd4ae3055b8c8a70507d8d",
"category_entries": "5affb6ddaf73ac7b14d9cc67f7d518d4bb8f280ee6d9f1ad852edd44bad8c7de",
"category_entries": "3ec30d2cb97f29514ff61898a4f23d2aa73a24b3468b6d410b1c2d18c8808927",
"choose_subscription": "33c04843d7c1b608d034e605e52681822fc6d79bc6b900c04915dd9ebae584e2",
"create_category": "6b22b5ce51abf4e225e23a79f81be09a7fb90acb265e93a8faf9446dff74018d",
"create_user": "1e940be3afefc0a5c6273bbadcddc1e29811e9548e5227ac2adfe697ca5ce081",
"edit_category": "daf073d2944a180ce5aaeb80b597eb69597a50dff55a9a1d6cf7938b48d768cb",
"edit_feed": "34aa0d668b3ea1a1b5fa480c20cebeae729b37010af3bb915d2a9eed73d3b996",
"edit_user": "f4f99412ba771cfca2a2a42778b023b413c5494e9a287053ba8cf380c2865c5f",
"entry": "1626bf4dd3223b2f730865676162aa0a9f0a0e009cdea90f705230542922e0f4",
"feed_entries": "4bb6b96ba4d13dbaf22dcf6dd95ae36b6e5a0c99175d502865a164dc68fd4bae",
"feeds": "d11fb629921e22bbf6d9ecb1adcc38922fafcee84f81c437abf47209544bd1c5",
"history_entries": "9763d2120cfaeb78d406fdc029197fed2f7cfa7682970eeedae82ae79be65519",
"entry": "e14434fc6f57963eae26057a18c835d0328af783d41f5af04b03387b4da604be",
"feed_entries": "9c70b82f55e4b311eff20be1641733612e3c1b406ce8010861e4c417d97b6dcc",
"feeds": "fa2dad422445eca898c1daa4ab742691207a8c0d3c274eed84462bc610d22219",
"history_entries": "87e17d39de70eb3fdbc4000326283be610928758eae7924e4b08dcb446f3b6a9",
"import": "5eb56cecaa4d369b9acc991a82be7617710c551089a2e99d34ce8b6e5c37df0a",
"integrations": "f85b4a48ab1fc13b8ca94bfbbc44bd5e8784f35b26a63ec32cbe82b96b45e008",
"login": "2e72d2d4b9786641b696bedbed5e10b04bdfd68254ddbbdb0a53cca621d200c7",
"search_entries": "d71849a4f2b0573c7c76ad0ea941812009e9f022de60895987a781d3e6f08a01",
"search_entries": "274950d03298c24f3942e209c0faed580a6d57be9cf76a6c236175a7e766ac6a",
"sessions": "1b3ec0970a4111b81f86d6ed187bb410f88972e2ede6723b9febcc4c7e5fc921",
"settings": "152143e58d057ea6ab3bfd8dd947bfd70685843ca40e40542484b23849746df4",
"unread_entries": "5c8c67d69da3e1d9437fdae967206b6dec84b241c806f32373071558f72d05d7",
"unread_entries": "e38f7ffce17dfad3151b08cd33771a2cefe8ca9db42df04fc98bd1d675dd6075",
"users": "4b56cc76fbcc424e7c870d0efca93bb44dbfcc2a08b685cf799c773fbb8dfb2f",
}

@ -7,10 +7,10 @@ package ui // import "miniflux.app/ui"
import (
"encoding/base64"
"errors"
"io/ioutil"
"net/http"
"time"
"miniflux.app/config"
"miniflux.app/crypto"
"miniflux.app/http/client"
"miniflux.app/http/request"
@ -37,24 +37,35 @@ func (h *handler) imageProxy(w http.ResponseWriter, r *http.Request) {
return
}
clt := client.New(string(decodedURL))
resp, err := clt.Get()
req, err := http.NewRequest("GET", string(decodedURL), nil)
if err != nil {
html.ServerError(w, r, err)
return
}
req.Header.Add("User-Agent", client.DefaultUserAgent)
req.Header.Add("Connection", "close")
if resp.HasServerFailure() {
clt := &http.Client{
Timeout: time.Duration(config.Opts.HTTPClientTimeout()) * time.Second,
}
resp, err := clt.Do(req)
if err != nil {
html.ServerError(w, r, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
html.NotFound(w, r)
return
}
body, _ := ioutil.ReadAll(resp.Body)
etag := crypto.HashFromBytes(body)
etag := crypto.HashFromBytes(decodedURL)
response.New(w ,r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) {
b.WithHeader("Content-Type", resp.ContentType)
b.WithBody(body)
response.New(w, r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) {
b.WithHeader("Content-Type", resp.Header.Get("Content-Type"))
b.WithBody(resp.Body)
b.WithoutCompression()
b.Write()
})

Loading…
Cancel
Save