Better solution to pr12145

This commit is contained in:
Jay McCarthy 2011-09-06 15:41:25 -06:00
parent ef84301f83
commit e9a9d79490
11 changed files with 59 additions and 49 deletions

View File

@ -80,22 +80,22 @@
(test-equal?* "file, exists, whole, no Range, get"
(collect (dispatch #t tmp-file) (req #f #"GET" empty))
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
(test-equal?* "file, exists, whole, no Range, head"
(collect (dispatch #t tmp-file) (req #f #"HEAD" empty))
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n")
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n")
(test-equal?* "file, exists, whole, Range, get"
(collect (dispatch #t tmp-file) (req #f #"GET" (list (make-header #"Range" #"bytes=0-80"))))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
(test-equal?* "file, exists, whole, Range, head"
(collect (dispatch #t tmp-file) (req #f #"HEAD" (list (make-header #"Range" #"bytes=0-80"))))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n")
(test-equal?* "file, exists, part, get"
(collect (dispatch #t tmp-file) (req #f #"GET" (list (make-header #"Range" #"bytes=5-9"))))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 5\r\nContent-Range: bytes 5-9/81\r\n\r\n><hea")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 5\r\nContent-Range: bytes 5-9/81\r\n\r\n><hea")
(test-equal?* "file, exists, part, head"
(collect (dispatch #t tmp-file) (req #f #"HEAD" (list (make-header #"Range" #"bytes=5-9"))))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 5\r\nContent-Range: bytes 5-9/81\r\n\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 5\r\nContent-Range: bytes 5-9/81\r\n\r\n")
(test-exn "path, non"
exn:dispatcher?
@ -103,16 +103,16 @@
(test-equal?* "dir, exists, no Range, get"
(collect (dispatch #t a-dir) (req #t #"GET" empty))
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
(test-equal?* "dir, exists, no Range, head"
(collect (dispatch #t a-dir) (req #t #"HEAD" empty))
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n")
#"HTTP/1.1 200 OK\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\n\r\n")
(test-equal?* "dir, exists, Range, get"
(collect (dispatch #t a-dir) (req #t #"GET" (list (make-header #"Range" #"bytes=0-80"))))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n<html><head><title>A title</title></head><body>Here's some content!</body></html>")
(test-equal?* "dir, exists, Range, head"
(collect (dispatch #t a-dir) (req #t #"HEAD" (list (make-header #"Range" #"bytes=0-80"))))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html; charset=utf-8\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nAccept-Ranges: bytes\r\nContent-Length: 81\r\nContent-Range: bytes 0-80/81\r\n\r\n")
(test-equal?* "dir, not dir-url, get"
(collect (dispatch #t a-dir) (req #f #"GET" empty))
#"HTTP/1.1 302 Moved Temporarily\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html\r\nConnection: close\r\nLocation: /foo/\r\n\r\n")

View File

@ -28,7 +28,7 @@ END
(check-not-false (read-mime-types test-file)))
(test-case
"Default mime-type given"
(check-equal? ((make-path->mime-type test-file) (build-path "test.html")) TEXT/HTML-MIME-TYPE))
(check-equal? ((make-path->mime-type test-file) (build-path "test.html")) #f))
(test-case
"MIME type resolves (single in file)"
(check-equal? ((make-path->mime-type test-file) (build-path "test.mp4")) #"video/mp4"))

View File

@ -36,6 +36,11 @@
(response 404 #"404" (current-seconds) #"text/html"
(list) void))
#"HTTP/1.1 404 404\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n")
(test-equi? "response"
(output output-response
(response 404 #"404" (current-seconds) #f
(list) void))
#"HTTP/1.1 404 404\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nConnection: close\r\n\r\n")
(test-equi? "response (header)"
(output output-response
(response 404 #"404" (current-seconds) #"text/html"
@ -231,12 +236,12 @@
(test-equi? "(get) multiple ranges"
(output output-file/boundary tmp-file #"GET" #"text/html" '((10 . 19) (30 . 39) (50 . 59)) #"BOUNDARY")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 260\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 10-19/81\r\n\r\nd><title>A\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 30-39/81\r\n\r\ntle></head\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 50-59/81\r\n\r\ne's some c\r\n--BOUNDARY--\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 266\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 10-19/81\r\n\r\nd><title>A\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 30-39/81\r\n\r\ntle></head\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 50-59/81\r\n\r\ne's some c\r\n\r\n--BOUNDARY--\r\n")
(test-equi? "(get) some bad ranges"
(parameterize ([current-error-port (open-output-nowhere)])
(output output-file/boundary tmp-file #"GET" #"text/html" '((10 . 19) (1000 . 1050) (30 . 39) (50 . 49)) #"BOUNDARY"))
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 178\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 10-19/81\r\n\r\nd><title>A\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 30-39/81\r\n\r\ntle></head\r\n--BOUNDARY--\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 182\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 10-19/81\r\n\r\nd><title>A\r\n\r\n--BOUNDARY\r\nContent-Type: text/html\r\nContent-Range: bytes 30-39/81\r\n\r\ntle></head\r\n\r\n--BOUNDARY--\r\n")
(test-equi? "(get) all bad ranges"
(parameterize ([current-error-port (open-output-nowhere)])
@ -281,11 +286,11 @@
(test-equi? "(head) multiple ranges"
(output output-file/boundary tmp-file #"HEAD" #"text/html" '((10 . 19) (30 . 39) (50 . 59)) #"BOUNDARY")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 260\r\n\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 266\r\n\r\n")
(test-equi? "(head) some bad ranges"
(output output-file/boundary tmp-file #"HEAD" #"text/html" '((10 . 19) (1000 . 1050) (30 . 39) (50 . 49)) #"BOUNDARY")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 178\r\n\r\n")
#"HTTP/1.1 206 Partial content\r\nDate: REDACTED GMT\r\nLast-Modified: REDACTED GMT\r\nServer: Racket\r\nContent-Type: multipart/byteranges; boundary=BOUNDARY\r\nAccept-Ranges: bytes\r\nContent-Length: 182\r\n\r\n")
(test-equi? "(head) all bad ranges"
(parameterize ([current-error-port (open-output-nowhere)])
@ -301,4 +306,4 @@
(get-output-string os))
"convert-http-ranges: No satisfiable ranges in ((-10 . -5) (1000 . 1050) (50 . 49))/81.")
))))
))))

View File

@ -14,7 +14,7 @@
[read-range-header (-> (listof header?) (or/c (listof pair?) false/c))]
[make
(->* (#:url->path url->path/c)
(#:path->mime-type (path-string? . -> . bytes?)
(#:path->mime-type (path-string? . -> . (or/c false/c bytes?))
#:indices (listof path-string?))
dispatcher/c)])
@ -27,7 +27,7 @@
(define interface-version 'v1)
(define (make #:url->path url->path
#:path->mime-type [path->mime-type (lambda (path) TEXT/HTML-MIME-TYPE)]
#:path->mime-type [path->mime-type (lambda (path) #f)]
#:indices [indices (list "index.html" "index.htm")])
(lambda (conn req)
(define uri (request-uri req))

View File

@ -24,8 +24,8 @@
([code number?]
[message bytes?]
[seconds number?]
[mime bytes?]
[mime (or/c false/c bytes?)]
[headers (listof header?)]
[output (output-port? . -> . void)])]
[response/full (-> number? bytes? number? bytes? (listof header?) (listof bytes?) response?)]
[response/full (-> number? bytes? number? (or/c false/c bytes?) (listof header?) (listof bytes?) response?)]
[TEXT/HTML-MIME-TYPE bytes?])

View File

@ -16,7 +16,7 @@
[print-headers (output-port? (listof header?) . -> . void)]
[rename ext:output-response output-response (connection? response? . -> . void)]
[rename ext:output-response/method output-response/method (connection? response? bytes? . -> . void)]
[rename ext:output-file output-file (connection? path-string? bytes? bytes? (or/c pair? false/c) . -> . void)])
[rename ext:output-file output-file (connection? path-string? bytes? (or/c bytes? false/c) (or/c pair? false/c) . -> . void)])
(define (output-response conn resp)
(output-response/method conn resp #"GET"))
@ -59,9 +59,13 @@
[#"Last-Modified"
(string->bytes/utf-8 (seconds->gmt-string (response-seconds bresp)))]
[#"Server"
#"Racket"]
[#"Content-Type"
(response-mime bresp)])
#"Racket"])
(if (response-mime bresp)
(maybe-headers
seen?
[#"Content-Type"
(response-mime bresp)])
empty)
(if (connection-close? conn)
(maybe-headers
seen?
@ -152,12 +156,12 @@
;; A boundary is generated only if a multipart/byteranges response needs
;; to be generated (i.e. if a Ranges header was specified with more than
;; one range in it).
(define (output-file conn file-path method mime-type ranges)
(define (output-file conn file-path method maybe-mime-type ranges)
(output-file/boundary
conn
file-path
method
mime-type
maybe-mime-type
ranges
(if (and ranges (> (length ranges) 1))
(md5 (string->bytes/utf-8 (number->string (current-inexact-milliseconds))))
@ -170,7 +174,7 @@
;; (U (listof (U byte-range-spec suffix-byte-range-spec)) #f)
;; (U bytes #f)
;; -> void
(define (output-file/boundary conn file-path method mime-type ranges boundary)
(define (output-file/boundary conn file-path method maybe-mime-type ranges boundary)
; total-file-length : integer
(define total-file-length
(file-size file-path))
@ -189,7 +193,7 @@
(exn-message exn))
(output-response-head
conn
(make-416-response modified-seconds mime-type)))])
(make-416-response modified-seconds maybe-mime-type)))])
(let* (; converted-ranges : (alist-of integer integer)
; This is a list of actual start and end offsets in the file.
; See the comments for convert-http-ranges for more information.
@ -203,7 +207,7 @@
; response. This *must be* the same length as converted-ranges.
[multipart-headers
(if (> (length converted-ranges) 1)
(prerender-multipart/byteranges-headers mime-type converted-ranges total-file-length)
(prerender-multipart/byteranges-headers maybe-mime-type converted-ranges total-file-length)
(list #""))]
; total-content-length : integer
[total-content-length
@ -226,8 +230,8 @@
(output-response-head
conn
(if ranges
(make-206-response modified-seconds mime-type total-content-length total-file-length converted-ranges boundary)
(make-200-response modified-seconds mime-type total-content-length)))
(make-206-response modified-seconds maybe-mime-type total-content-length total-file-length converted-ranges boundary)
(make-200-response modified-seconds maybe-mime-type total-content-length)))
; Send the appropriate file content:
(when (bytes-ci=? method #"GET")
(adjust-connection-timeout! ; Give it one second per byte.
@ -261,13 +265,14 @@
(loop rest (cdr multipart-headers))]))))))))))
;; prerender-multipart/byteranges-headers : bytes (alist-of integer integer) integer -> (list-of bytes)
(define (prerender-multipart/byteranges-headers mime-type converted-ranges total-file-length)
(define (prerender-multipart/byteranges-headers maybe-mime-type converted-ranges total-file-length)
(map (lambda (range)
(match range
[(list-rest start end)
(let ([out (open-output-bytes)])
(print-headers out (list (make-header #"Content-Type" mime-type)
(make-content-range-header start end total-file-length)))
(when maybe-mime-type
(print-headers out (list (make-header #"Content-Type" maybe-mime-type))))
(print-headers out (list (make-content-range-header start end total-file-length)))
(begin0 (get-output-bytes out)
(close-output-port out)))]))
converted-ranges))
@ -326,14 +331,14 @@
converted))
;; make-206-response : integer bytes integer integer (alist-of integer integer) bytes -> basic-response
(define (make-206-response modified-seconds mime-type total-content-length total-file-length converted-ranges boundary)
(define (make-206-response modified-seconds maybe-mime-type total-content-length total-file-length converted-ranges boundary)
(if (= (length converted-ranges) 1)
(let ([start (caar converted-ranges)]
[end (cdar converted-ranges)])
(response
206 #"Partial content"
modified-seconds
mime-type
maybe-mime-type
(list (make-header #"Accept-Ranges" #"bytes")
(make-content-length-header total-content-length)
(make-content-range-header start end total-file-length))
@ -347,21 +352,21 @@
void)))
;; make-200-response : integer bytes integer -> basic-response
(define (make-200-response modified-seconds mime-type total-content-length)
(define (make-200-response modified-seconds maybe-mime-type total-content-length)
(response
200 #"OK"
modified-seconds
mime-type
maybe-mime-type
(list (make-header #"Accept-Ranges" #"bytes")
(make-content-length-header total-content-length))
void))
;; make-416-response : integer bytes -> basic-response
(define (make-416-response modified-seconds mime-type)
(define (make-416-response modified-seconds maybe-mime-type)
(response
416 #"Invalid range request"
modified-seconds
mime-type
maybe-mime-type
null
void))

View File

@ -28,5 +28,5 @@
(provide/contract
[response/xexpr
((pretty-xexpr/c)
(#:code number? #:message bytes? #:seconds number? #:mime-type bytes? #:cookies (listof cookie?) #:headers (listof header?) #:preamble bytes?)
(#:code number? #:message bytes? #:seconds number? #:mime-type (or/c false/c bytes?) #:cookies (listof cookie?) #:headers (listof header?) #:preamble bytes?)
. ->* . response?)])

View File

@ -6,7 +6,7 @@
web-server/http)
(provide/contract
[read-mime-types (path-string? . -> . (hash/c symbol? bytes?))]
[make-path->mime-type (path-string? . -> . (path? . -> . bytes?))])
[make-path->mime-type (path-string? . -> . (path? . -> . (or/c false/c bytes?)))])
; read-mime-types : path? -> hash-table?
(define (read-mime-types a-path)
@ -40,5 +40,5 @@
[(regexp #rx#".*\\.([^\\.]*$)" (list _ sffx))
(hash-ref (force MIME-TYPE-TABLE)
(lowercase-symbol! sffx)
TEXT/HTML-MIME-TYPE)]
[_ TEXT/HTML-MIME-TYPE])))
#f)]
[_ #f])))

View File

@ -316,7 +316,7 @@ a URL that refreshes the password file, servlet cache, etc.}
It defines a dispatcher construction procedure.}]{
@defproc[(make [#:url->path url->path url->path/c]
[#:path->mime-type path->mime-type (path? . -> . bytes?) (lambda (path) TEXT/HTML-MIME-TYPE)]
[#:path->mime-type path->mime-type (path? . -> . (or/c false/c bytes)?) (lambda (path) #f)]
[#:indices indices (listof string?) (list "index.html" "index.htm")])
dispatcher/c]{
Uses @racket[url->path] to extract a path from the URL in the request

View File

@ -164,7 +164,7 @@ Here is an example typical of what you will find in many applications:
([code number?]
[message bytes?]
[seconds number?]
[mime bytes?]
[mime (or/c false/c bytes?)]
[headers (listof header?)]
[output (output-port? . -> . void)])]{
An HTTP response where @racket[output] produces the body. @racket[code] is the response code,
@ -182,7 +182,7 @@ Here is an example typical of what you will find in many applications:
]
}
@defproc[(response/full [code number?] [message bytes?] [seconds number?] [mime bytes?]
@defproc[(response/full [code number?] [message bytes?] [seconds number?] [mime (or/c false/c bytes?)]
[headers (listof header?)] [body (listof bytes?)])
response?]{
A constructor for responses where @racket[body] is the response body.
@ -481,7 +481,7 @@ web-server/insta
[#:code code number? 200]
[#:message message bytes? #"Okay"]
[#:seconds seconds number? (current-seconds)]
[#:mime-type mime-type bytes? TEXT/HTML-MIME-TYPE]
[#:mime-type mime-type (or/c false/c bytes?) TEXT/HTML-MIME-TYPE]
[#:headers headers (listof header?) empty]
[#:cookies cookies (listof cookie?) empty]
[#:preamble preamble bytes? #""])

View File

@ -17,7 +17,7 @@ files.
}
@defproc[(make-path->mime-type [p path-string?])
(path? . -> . bytes?)]{
(path? . -> . (or/c false/c bytes?))]{
Uses a @racket[read-mime-types] with @racket[p] and constructs a
function from paths to their MIME type.
}