clojurescript

Kari Marttila 2025-03-07T07:50:19.408589Z

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

Kari Marttila 2025-03-07T08:11:21.807829Z

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...

p-himik 2025-03-07T09:31:49.132139Z

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.

Kari Marttila 2025-03-07T10:31:34.465409Z

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.

Kari Marttila 2025-03-07T10:34:28.551889Z

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)
  

Kari Marttila 2025-03-07T10:38:05.323939Z

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...

Kari Marttila 2025-03-07T10:42:35.808679Z

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 ...

Kari Marttila 2025-03-07T10:43:12.051689Z

Good that I asked here. At least I know how to proceed.

Kari Marttila 2025-03-07T10:45:20.308679Z

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

p-himik 2025-03-07T10:46:21.936419Z

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.

Kari Marttila 2025-03-07T10:47:37.202439Z

Ah, I didn't think about that.

p-himik 2025-03-07T10:49:20.523159Z

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))

Kari Marttila 2025-03-07T10:53:55.682259Z

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.

p-himik 2025-03-07T10:54:08.045339Z

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.

p-himik 2025-03-07T10:54:38.992429Z

> 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.

Kari Marttila 2025-03-07T10:54:54.239849Z

Ok. Thanks so much! I try!

👍 1
Kari Marttila 2025-03-07T11:02:45.326289Z

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?

p-himik 2025-03-07T11:05:16.181309Z

> 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.

Kari Marttila 2025-03-07T15:29:19.854559Z

Ok. I figured out the problem. Rookie mistake. Wrong content type. 😞

👍 1
Kari Marttila 2025-03-07T07:52:09.433019Z

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.