1
0

fetch_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package fetch
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "strings"
  9. "sync/atomic"
  10. "testing"
  11. "time"
  12. )
  13. // testFetcher returns a Fetcher with retries that don't slow tests down.
  14. func testFetcher() *Fetcher {
  15. f := New(true)
  16. f.Backoff = time.Millisecond
  17. return f
  18. }
  19. func serve(t *testing.T, h http.HandlerFunc) *httptest.Server {
  20. t.Helper()
  21. srv := httptest.NewServer(h)
  22. t.Cleanup(srv.Close)
  23. return srv
  24. }
  25. func mustParse(t *testing.T, raw string) *url.URL {
  26. t.Helper()
  27. u, err := url.Parse(raw)
  28. if err != nil {
  29. t.Fatal(err)
  30. }
  31. return u
  32. }
  33. func TestFetchHTML(t *testing.T) {
  34. var gotUA, gotAccept string
  35. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  36. gotUA, gotAccept = r.UserAgent(), r.Header.Get("Accept")
  37. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  38. w.Write([]byte("<p>Héllo</p>"))
  39. })
  40. page, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL+"/a"))
  41. if err != nil {
  42. t.Fatal(err)
  43. }
  44. if page.HTML != "<p>Héllo</p>" {
  45. t.Errorf("HTML = %q", page.HTML)
  46. }
  47. if page.URL.String() != srv.URL+"/a" {
  48. t.Errorf("URL = %q", page.URL)
  49. }
  50. if !strings.Contains(gotUA, "rekopub") {
  51. t.Errorf("User-Agent = %q", gotUA)
  52. }
  53. if !strings.HasPrefix(gotAccept, "text/html") {
  54. t.Errorf("Accept = %q", gotAccept)
  55. }
  56. }
  57. func TestFetchFollowsRedirects(t *testing.T) {
  58. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  59. if r.URL.Path == "/old" {
  60. http.Redirect(w, r, "/new", http.StatusMovedPermanently)
  61. return
  62. }
  63. w.Header().Set("Content-Type", "text/html")
  64. w.Write([]byte("<p>moved</p>"))
  65. })
  66. page, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL+"/old"))
  67. if err != nil {
  68. t.Fatal(err)
  69. }
  70. if page.URL.Path != "/new" {
  71. t.Errorf("final URL = %q, want path /new", page.URL)
  72. }
  73. }
  74. func TestFetchNotFoundIsNotRetried(t *testing.T) {
  75. var calls atomic.Int32
  76. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  77. calls.Add(1)
  78. http.NotFound(w, r)
  79. })
  80. _, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL))
  81. var se *StatusError
  82. if !errors.As(err, &se) || se.Code != http.StatusNotFound {
  83. t.Fatalf("error = %v, want a 404 StatusError", err)
  84. }
  85. if !strings.Contains(err.Error(), "returned 404 Not Found") {
  86. t.Errorf("error = %q", err)
  87. }
  88. if n := calls.Load(); n != 1 {
  89. t.Errorf("server called %d times, want 1", n)
  90. }
  91. }
  92. func TestFetchRetries(t *testing.T) {
  93. for _, code := range []int{http.StatusTooManyRequests, http.StatusServiceUnavailable} {
  94. var calls atomic.Int32
  95. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  96. if calls.Add(1) < 3 {
  97. w.Header().Set("Retry-After", "0")
  98. w.WriteHeader(code)
  99. return
  100. }
  101. w.Header().Set("Content-Type", "text/html")
  102. w.Write([]byte("<p>ok</p>"))
  103. })
  104. page, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL))
  105. if err != nil {
  106. t.Fatalf("%d: %v", code, err)
  107. }
  108. if page.HTML != "<p>ok</p>" || calls.Load() != 3 {
  109. t.Errorf("%d: HTML = %q after %d calls, want ok after 3", code, page.HTML, calls.Load())
  110. }
  111. }
  112. }
  113. func TestFetchGivesUpAfterRetries(t *testing.T) {
  114. var calls atomic.Int32
  115. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  116. calls.Add(1)
  117. w.WriteHeader(http.StatusInternalServerError)
  118. })
  119. _, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL))
  120. var se *StatusError
  121. if !errors.As(err, &se) || se.Code != http.StatusInternalServerError {
  122. t.Fatalf("error = %v, want a 500 StatusError", err)
  123. }
  124. if n := calls.Load(); n != 3 {
  125. t.Errorf("server called %d times, want 3 (1 + 2 retries)", n)
  126. }
  127. }
  128. func TestFetchRejectsNonHTML(t *testing.T) {
  129. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  130. w.Header().Set("Content-Type", "application/pdf")
  131. w.Write([]byte("%PDF-1.7"))
  132. })
  133. _, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL))
  134. if err == nil || !strings.Contains(err.Error(), "not a web page") {
  135. t.Fatalf("error = %v, want a not-a-web-page error", err)
  136. }
  137. }
  138. func TestFetchSniffsMissingContentType(t *testing.T) {
  139. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  140. w.Header()["Content-Type"] = nil // stop net/http from adding one
  141. w.Write([]byte("<!DOCTYPE html><html><body><p>sniffed</p></body></html>"))
  142. })
  143. page, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL))
  144. if err != nil {
  145. t.Fatal(err)
  146. }
  147. if !strings.Contains(page.HTML, "sniffed") {
  148. t.Errorf("HTML = %q", page.HTML)
  149. }
  150. }
  151. func TestFetchSniffsOctetStream(t *testing.T) {
  152. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  153. w.Header().Set("Content-Type", "application/octet-stream")
  154. if r.URL.Path == "/page" {
  155. w.Write([]byte("<!DOCTYPE html><html><body><p>sniffed</p></body></html>"))
  156. } else {
  157. w.Write([]byte("%PDF-1.7 binary"))
  158. }
  159. })
  160. page, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL+"/page"))
  161. if err != nil || !strings.Contains(page.HTML, "sniffed") {
  162. t.Errorf("HTML page: err = %v", err)
  163. }
  164. if _, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL+"/file")); err == nil {
  165. t.Error("binary file was accepted as a web page")
  166. }
  167. }
  168. func TestFetchRejectsLargePages(t *testing.T) {
  169. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  170. w.Header().Set("Content-Type", "text/html")
  171. w.Write([]byte(strings.Repeat("x", 2<<20)))
  172. })
  173. f := testFetcher()
  174. f.MaxBytes = 1 << 20
  175. _, err := f.Fetch(context.Background(), mustParse(t, srv.URL))
  176. if err == nil || !strings.Contains(err.Error(), "larger than 1 MB") {
  177. t.Fatalf("error = %v, want a size error", err)
  178. }
  179. }
  180. func TestFetchDecodesCharsets(t *testing.T) {
  181. tests := []struct {
  182. name string
  183. contentType string
  184. body []byte
  185. want string
  186. }{
  187. {
  188. name: "ISO-8859-1 from header",
  189. contentType: "text/html; charset=iso-8859-1",
  190. body: []byte("<p>caf\xe9</p>"),
  191. want: "<p>café</p>",
  192. },
  193. {
  194. name: "Shift_JIS from meta tag",
  195. contentType: "text/html",
  196. body: append([]byte(`<html><head><meta charset="shift_jis"></head><body><p>`), append([]byte("\x93\xfa\x96\x7b"), []byte("</p></body></html>")...)...),
  197. want: "<p>日本</p>",
  198. },
  199. {
  200. name: "windows-1252 quotes",
  201. contentType: "text/html; charset=windows-1252",
  202. body: []byte("<p>\x93quoted\x94</p>"),
  203. want: "<p>“quoted”</p>",
  204. },
  205. }
  206. for _, tt := range tests {
  207. t.Run(tt.name, func(t *testing.T) {
  208. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  209. w.Header().Set("Content-Type", tt.contentType)
  210. w.Write(tt.body)
  211. })
  212. page, err := testFetcher().Fetch(context.Background(), mustParse(t, srv.URL))
  213. if err != nil {
  214. t.Fatal(err)
  215. }
  216. if !strings.Contains(page.HTML, tt.want) {
  217. t.Errorf("HTML = %q, want it to contain %q", page.HTML, tt.want)
  218. }
  219. })
  220. }
  221. }
  222. func TestFetchCancelled(t *testing.T) {
  223. srv := serve(t, func(w http.ResponseWriter, r *http.Request) {
  224. w.WriteHeader(http.StatusServiceUnavailable)
  225. })
  226. f := testFetcher()
  227. f.Backoff = time.Hour // cancellation must interrupt the wait
  228. ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
  229. defer cancel()
  230. start := time.Now()
  231. _, err := f.Fetch(ctx, mustParse(t, srv.URL))
  232. if !errors.Is(err, context.DeadlineExceeded) {
  233. t.Fatalf("error = %v, want context.DeadlineExceeded", err)
  234. }
  235. if time.Since(start) > 5*time.Second {
  236. t.Errorf("Fetch took %v after cancellation", time.Since(start))
  237. }
  238. }