Showing PDF document using Clojurescript. I have some issue with this, and appreciate some help if someone knows how to do this properly with Clojurescript and hiccup. I documented what I have done and how I debugged the issue in the #replicant 🧵 here: https://clojurians.slack.com/archives/C06JZ4X334N/p1741332913374909
Hm. I try another solution. In the backend, generate a public url for the document, provide it to frontend, and ask frontend to show that...
Just in case, to preclude the XY problem - do you really need to embed PDFs? Is the browser's functionality of downloading files or opening PDFs in a separate tab without downloading them as a file not enough?
And if you do need embedding - why do you fetch the PDF? Why not just use that URL in the src attribute of an embed tag?
You also mentioned that the data, after you download the PDF, becomes a string. It it really a string? Because if so, something is terribly wrong. It should be a binary blob, if anything.
You are right. I don't need to embed it. I just tried to show the pdf blob and I am beginning to think it is broken when it arrives to my frontend.
I experimented in the frontend REPL like this:
(defn display-pdf [pdf-blob]
(let [blob (js/Blob. #js [pdf-blob] #js {:type "application/pdf"})
blob-url (.createObjectURL js/URL blob)
]
(.open js/window blob-url)))
(defn fetch-pdf-blob [url data]
(go
(let [response (<! (http/post url {:json-params data }))]
(if (= 200 (:status response))
(let [pdf-blob (gobj/get response "body")]
(display-pdf pdf-blob))
(js/console.error "Failed to fetch PDF" (:status response))))))
(def my-data {:s3-uri ""})
(fetch-pdf-blob "/queryapp/api/v1/model/reference" my-data)
I am using a kind of data viewer (https://github.com/cjohansen/gadget-inspector ) - I guess that shows it as string. But I guess it is binary. Let me see my atom...
Yep. That must be the reason:
(def my-data (:data (:reference (:db/data @!state))))
(type my-data)
;;=> #object [String]
my-data
;;=> "%PDF-1.4\n%����\n1 0 obj\n<</Title <FEFF004A006F00 ...Good that I asked here. At least I know how to proceed.
I would fetch the damn PDF as a url if I could. 🙂 But I guess the Cognitect AWS sdk does not support presigned urls, I asked this in the #aws channel: https://clojurians.slack.com/archives/C09N0H1RB/p1741336871468699
Why not just proxy the URL through your backend? Your frontend would just access your backend and would have no idea and no care where the PDF came from.
Ah, I didn't think about that.
The only trouble is cleaning up the headers. I use this:
(defn- cleanup-aws-headers [headers]
(reduce-kv (fn [acc h v]
(let [h (str/lower-case h)]
(case h
"access-control-allow-headers"
(assoc acc h
(->> (str/split v #",")
(remove #(let [v (str/lower-case %)]
(or (= v "authorization")
(str/starts-with? v "x-amz-"))))))
"access-control-allow-methods"
(assoc acc h "HEAD,GET,OPTIONS")
(cond-> acc
;; Remove all AWS-specific headers.
(not (or (#{"server"
"access-control-expose-headers"
"access-control-allow-origin"} h)
(str/starts-with? h "x-amz-")))
(assoc h v)))))
{} headers))Hm. If I don't have the presigned url for the document, I don't quite get, how to get the document from the backend with this proxy idea.
Here's some extra code:
(defn encode-content-disposition-file-name [^String file-name]
(-> file-name
;; For some reason, at least Google Chrome doesn't handle
;; \tab, %2F (/) and %5C (\) very well in
;; content-disposition file name
(.replace \tab \space)
(.replace \\ \_)
(.replace \/ \_)
(URLEncoder/encode "UTF-8")
(.replace "+" "%20")))
(defn content-disposition-header [disposition ^String file-name]
(str disposition
"; filename=\"" (encode-content-disposition-file-name (Normalizer/normalize file-name Normalizer$Form/NFKD))
"\"; filename*=utf-8''" (encode-content-disposition-file-name file-name)))
(defn not-found? [result]
(= (:cognitect.anomalies/category result) :cognitect.anomalies/not-found))
(defn proxy-object-request [client key bucket {:keys [request file-name not-found-msg not-found-ctx]}]
(let [{:strs [range if-none-match if-modified-since]} (:headers request)
if-modified-since (some-> if-modified-since ring.util.time/parse-date)
result (get-object-waiting client (cond-> {:Bucket bucket
:Key (str key)}
if-modified-since (assoc :IfModifiedSince if-modified-since)
if-none-match (assoc :IfNoneMatch if-none-match)
range (assoc :Range range)))
{:keys [status headers]} (-> result meta :http-response)
headers (-> headers
(cleanup-aws-headers)
(assoc "cache-control"
;; Despite its name, the `no-cache` value actually
;; means "validate the cached value before using".
"no-cache"))
headers (cond-> headers
file-name
(assoc "content-disposition"
(content-disposition-header
;; We want to be able to display the resource within the browser
;; as often as possible. If the browser cannot display some particular
;; file, it will just default to downloading it.
"inline"
file-name)))]
(if (not-found? result)
(res/not-found (str (or not-found-msg "The requested object does not exist") " " not-found-ctx))
(do (check-aws-result result)
{:body (:Body result)
:status status
:headers headers}))))
get-object-waiting is just a wrapper around :GetObject that retries with a timeout since attempting to immediately retrieve an object that was just written can result in 404. Well, it used to be the case years ago anyway. You probably don't need it.
check-aws-result checks for any anomalies and wraps the error in a way I want.> If I don't have the presigned url for the document, I don't quite get, how to get the document from the backend with this proxy idea. Well, how does the backend get anything from S3? Same way.
Ok. Thanks so much! I try!
One question. Did I get this idea right. The proxy-object-request is an api in the backend? How the frontend call this api? I guess we cannot pass the s3-client as a parameter?
> The proxy-object-request is an api in the backend?
Yes.
> How the frontend call this api?
You use that function in a (probably new) HTTP API endpoint. That request argument is the original Ring request.
Ok. I figured out the problem. Rookie mistake. Wrong content type. 😞
I think the reason is not related with Replicant, but I documented the question there since I have gotten good help earlier from the Replicant developers, and realized only after that possibly the right place would have been to document the issue in the #clojurescript channel.