Git Product home page Git Product logo

re-frame-http-fx's Introduction

Clojars Project GitHub issues License

HTTP Effects Handler For re-frame

This re-frame library contains an HTTP Effect Handler.

Keyed :http-xhrio, it wraps the goog xhrio API of cljs-ajax.

IMPORTANT: This effect handler depends entirely on the API of cljs-ajax. Make sure you are familiar with the API for cljs-ajax, and especially with ajax-request before proceeding.

Quick Start Guide

Step 1. Add Dependency

Add the following project dependency:
clojars

Requires re-frame >= 0.8.0

Step 2. Registration And Use

In the namespace where you register your event handlers, perhaps called events.cljs, you have 2 things to do.

First, add this "require" to the ns:

(ns app.core
  (:require
    ...
    [day8.re-frame.http-fx]   ;; <-- add this
    ...))

Because we never subsequently use this require, it appears redundant. But its existence will cause the :http-xhrio effect handler to self-register with re-frame, which is important to everything that follows.

Second, write a an event handler which uses this effect:

(ns app.events              ;; or where ever you define your event handlers
  (:require
    ...
    [ajax.core :as ajax]    ;; so you can use this in the response-format below
    ...))
    
(reg-event-fx                             ;; note the trailing -fx
  :handler-with-http                      ;; usage:  (dispatch [:handler-with-http])
  (fn [{:keys [db]} _]                    ;; the first param will be "world"
    {:db   (assoc db :show-twirly true)   ;; causes the twirly-waiting-dialog to show??
     :http-xhrio {:method          :get
                  :uri             "https://api.github.com/orgs/day8"
                  :timeout         8000                                           ;; optional see API docs
                  :response-format (ajax/json-response-format {:keywords? true})  ;; IMPORTANT!: You must provide this.
                  :on-success      [:good-http-result]
                  :on-failure      [:bad-http-result]}}))

Look at the :http-xhrio line above. This library defines the "effects handler" which implements :http-xhrio.

The supplied value should be an options map as defined by the simple interface ajax-request see: api docs. Except for :on-success and :on-failure. All options supported by ajax-request should be supported by this library, as it is a thin wrapper over ajax-request.

Here is an example of a POST request. Note that :format also needs to be specified (unless you pass :body in the map).

(re-frame/reg-event-fx
  ::http-post
  (fn [_world [_ val]]
    {:http-xhrio {:method          :post
                  :uri             "https://httpbin.org/post"
                  :params          data
                  :timeout         5000
                  :format          (ajax/json-request-format)
                  :response-format (ajax/json-response-format {:keywords? true})
                  :on-success      [::success-post-result]
                  :on-failure      [::failure-post-result]}}))

N.B.: ajax-request is harder to use than the GET and POST functions cljs-ajax provides, but this gives you smaller code sizes from dead code elimination. In particular, you MUST provide a :response-format, it is not inferred for you.

Don't provide:

 :api     - the effects handler explicitly uses xhrio so it will be ignored.
 :handler - we substitute this with one that dispatches `:on-success` or `:on-failure` events.

You can also pass a list or vector of these options maps where multiple HTTPs are required.

To make multiple requests, supply a vector of options maps:

{:http-xhrio [ {...}
               {...}]}

Step 3a. Handling :on-success

Provide normal re-frame handlers for :on-success and :on-failure. Your event handlers will get the result as the last argument of their event vector. Here is an example written as another effect handler to put the result into db.

(reg-event-db
  ::success-http-result
  (fn [db [_ result]]
    (assoc db :success-http-result result)))

Step 3b. Handling :on-failure

The result supplied to your :on-failure handler will be a map containing various xhrio details (details below). See the fn ajax-xhrio-handler for details

Step 3.1 :on-failure result

A simple failure handler could be written this way ...

(reg-event-db
  ::failure-http-result
  (fn [db [_ result]]
    ;; result is a map containing details of the failure
    (assoc db :failure-http-result result)))
status of 40x/50x

If the network connection to the server is successful, but the server returns an error (40x/50x) HTTP status code result will be a map like:

{:uri "/error"
 :last-method "GET"
 :last-error "Service Unavailable [503]"
 :last-error-code 6
 :debug-message "Http response at 400 or 500 level"
 :status 503
 :status-text "Service Unavailable"
 :failure :error
 :response nil}
Status 0

In some cases, if the network connection itself is unsuccessful, it is possible to get a status code of 0. For example:

  • cross-site scripting whereby access is denied; or
  • requesting a URI that is unreachable (typo, DNS issues, invalid hostname etc); or
  • request is interrupted after being sent (browser refresh or navigates away from the page); or
  • request is otherwise intercepted (check your ad blocker).

In this case, result will be something like:

{:uri "http://i-do-not-exist/error"
 :last-method "GET"
 :last-error " [0]"
 :last-error-code 6
 :debug-message "Http response at 400 or 500 level"
 :status 0
 :status-text "Request failed."
 :failure :failed}
Status -1

If the time for the sever to respond exceeds :timeout result will be a map something like:

{:uri "/timeout"
 :last-method "GET"
 :last-error "Timed out after 1ms, aborting"
 :last-error-code 8
 :debug-message "Request timed out"
 :status -1
 :status-text "Request timed out."
 :failure :timeout}

Optional: Handler for :on-request

If you need access to the raw request, to for example, cancel long running requests or repeated debounced requests, you can pass an :on-request handler that will be called with the request.

(re-frame/reg-event-fx
  ::http-post
  (fn [_world [_ val]]
    {:http-xhrio {:method          :get
                  :uri             "https://httpbin.org/delay/60"
                  :format          (ajax/json-request-format)
                  :response-format (ajax/json-response-format {:keywords? true})
                  :on-request      [::track-slow-request "my-request"]
                  :on-success      [::success-get-result]
                  :on-failure      [::failure-get-result]}}))

(reg-event-db
  ::track-slow-request
  (fn [db [_ my-id xhrio]]
    (assoc-in db [:requests my-id] xhrio)))

Later if you need to, you could retrieve the request from the app-db and cancel it.

N.B.: To prevent memory leaks you need to cleanup the request in both your :on-success and :on-failure handlers. Otherwise the requests will just hang around in your app-db indefinitely.

Tip

If you need additional arguments or identifying tokens in your handler, then include them in your :on-success and :on-failure event vector in Step 3.

For example ...

(re-frame/reg-event-fx
  ::http-post
  (fn [_ [_ val]]
    {:http-xhrio {:method          :post
                  ...
                  :on-success      [::success-post-result 42 "other"]
                  :on-failure      [::failure-post-result :something :else]}}))

Notice the way that additional values are encoded into the success and failure event vectors.

These event vectors will be dispatched (result is conj-ed to the end) making all encoded values AND the result available to the handlers.

re-frame-http-fx's People

Contributors

danielcompton avatar dijonkitchen avatar egli avatar hipitihop avatar idmitrievsky avatar kennethkalmer avatar kennyjwilli avatar madstap avatar michaelcameron avatar mike-thompson-day8 avatar nathell avatar raymcdermott avatar rt567 avatar superstructor avatar taylorwood avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

re-frame-http-fx's Issues

ajax/json-request-format with namespace keywords

This request formatter is stripping any leading namespace qualified keywords before writing to json. {:widget/id 123} becomes {:id 123}. We are using the qualified keywords in our system. mitosin on the back end send json response perfectly fine with the ns qualified keywords. Our current work around is to send to the backend using transit. However we don't need any of the features of transit and its just making it hard to debug requests from the console.

It looks like its from the default keyword fn in clj->js.

(defn- nsq-keyword-fn [k]
  (let [ns (namespace k)
        n  (name k)]
    (if ns
      (str ns "/" n)
      n)))

(clj->js {:a/b 123} :keyword-fn nsq-keyword-fn) => #js {"a/b" 123}

I'm not exactly sure how to stick that into json.cljc as an option to json-request-format.

Warning when compiling with infer-externs enabled

This is a case of the same bug as day8/re-frame#512: here, the use of js/goog.net.Xhrio instead of just goog.net.Xhrio in request->xhrio-options triggers a warning when using advanced optimisations with infer-externs enabled:

WARNING: out/inferred_externs.js:9: WARNING - name goog is not defined in the externs.
goog.net;
^^^^

Jul 22, 2019 4:47:01 PM com.google.javascript.jscomp.LoggerErrorManager println
WARNING: out/inferred_externs.js:10: WARNING - name goog is not defined in the externs.
goog.net.Xhrio;
^^^^

Does not work without :format provided for method :get

I am using an example provided in README.

(rf/reg-event-db
:good-http-result
(fn [db [_ result]]
(println db)
(assoc db :api-result result)))

(rf/reg-event-db
:bad-http-result
(fn [db [_ result]]
(assoc db :api-result result)))

(rf/reg-event-fx ;; note the trailing -fx
:handler-with-http ;; usage: (dispatch [:handler-with-http])
(fn [{:keys [db]} _] ;; the first param will be "world"
{:db (assoc db :show-twirly true) ;; causes the twirly-waiting-dialog to show??
:http-xhrio {:method :get
:uri "https://api.github.com/orgs/day8"
:timeout 8000
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:good-http-result]
:on-failure [:bad-http-result]}}))

In browser I get "Error: unrecognised format"

If I specify format: ":format :json" (this and other endpoints I tried return json), then I get nil when I print db in :good-http-result

Pass original event id to :on-success and :on-failure

For logging and other events, its good to track the to original caller of the http request. Since internally, on-* is re-dispacted the event name in the fx [{:keys [db]} [event-name response]] is that of the event specified in on-*. For specific events and success handlers, I can obviously pass in the event name of the caller. However this breaks down for generic handlers, and the default fallback events.

Passing works if possible

(rf/reg-event-fx
  :on-failure
  (fn [{:keys [db]} [event-name calling-event response]]
    ...))

For example, our default on-failure warns that the error should be handled. It's hard to track down where the warning is thrown from because I can't pull the original calling event name.

(rf/reg-event-db
  :http-no-on-failure
  (fn [db]
    (js/console.warn "Uncaught xhrio failure.  Consider catching and setting loading state with :basic-xhrio-failure")))

Make it possible to abort requests

It's common to want to abort an in-progress HTTP request, perhaps if a more recent request comes along (for example the case of multiple autocomplete requests), or if the user navigates away and it is no longer relevant.

How might I go about holding on to handlers (perhaps in :db) so that I can later handle them? Or would it be better to have an API that allowed a programmer to specify some sort of identifier for each request, such that multiple requests with the same identifier caused the previous one to abort?

unrecognized response format: nil

Playing around with a simple example yields this error:

Uncaught Error: ["unrecognized response format: " nil]

ajax$core$throw_error
ajax$core$get_response_forma
ajax$core$normalize_request
...

I strongly suspect that cljs-ajax.core/ajax-request needs a :response-format key as seen in the example. Looking at the relevant lines in core.cljc it seems that there is no default response-format onto which it could fall back; it therefore always throws an error when not response-format is provided.

Possible fixes could be:
a) Providing the key in the map passed to :http-xhrio. However, my Clojure-Fu is not yet good enough to think of a way without clumsily including cljs.ajax.core in my re-frame handlers to access the various response-format-types..
b) Set some sort of default format and add it to the map.

Hope that helps! If you need anything further, let me know!

Separate handler for :on-timeout?

Hey @danielcompton, I'm wondering if you think there's any merit to adding a separate handler specifically for if the timeout you specify is hit? Happy to submit a patch. FWIW I began using this yesterday and think it'd be useful.

What counts as success?

Some situations where I'd expect success but receive failure:

(reg-event-fx
 :cms/test-200
 (fn [{:keys [db]} [_]]
   {:http-xhrio {:method :post
                 :uri "https://httpbin.org/status/200"
                 :timeout 5000
                 :params {}
                 :format (ajax/json-request-format)
                 :response-format (ajax/json-response-format {:keyword? true})
                 :on-success [:success-handler]
                 :on-failure [:failure-handler]}}))

(reg-event-fx
 :cms/test-201
 (fn [{:keys [db]} [_]]
   {:http-xhrio {:method :post
                 :uri "https://httpbin.org/status/201"
                 :timeout 5000
                 :params {}
                 :format (ajax/json-request-format)
                 :response-format (ajax/json-response-format {:keyword? true})
                 :on-success [:success-handler]
                 :on-failure [:failure-handler]}}))

Seems like just a 200 or 201 response is not enough to be considered a success.

bad request format, when i use :method :delete.

I try to make simple crud app and have 4 handlers on my backend for each type of request-methods: POST, PUT, UPDATE and DELETE , how i cant send DELETE request to the server wtih http-fx?

Provide default response-format?

Currently, people need to provide a response format with every request, by requiring ajax.core and calling (ajax/json-response-format {:keywords? true}) or similar. cljs-ajax doesn't allow using keywords in ajax-call to lookup response formats because that will defeat dead code elimination. I can see two options:

  1. Create an atom where people can set a default response format
  2. Change the reg-fx to be inside a function, and let people call it with a default response format as a parameter. This could also be extended to let people provide default interceptors and other default parameters in the request map.

I'm probably leaning towards 2, but interested in other feedback.

Prepare shift from XhrIo?

As commented on #18, the next version of cljs-ajax is supposed to drop XhrIo.

It is intended that the default implementation for JavaScript uses XmlHttpRequest directly and XhrIO is deprecated in 0.8. Be warned.

Is this effect handler going to be updated to use the different backend, or will it drop cljs-ajax?

compilation failed for 1.1.0

output below:

Failed to compile "resources/public/js/compiled/app.js" in 27.16 seconds.
----  Could not Analyze  resources/public/js/compiled/out/day8/re_frame/http_fx.cljs   line:63  column:79  ----

  No value supplied for key: []

  62  (s/def ::request-map (s/keys :req-un [::method ::uri ::response-format ::on-success ::on-failure]))
  63  (s/def ::sequential-or-map (s/or :request-map ::request-map :seq-request-maps (s/coll-of ::request-map [])))
                                                                                    ^--- No value supplied for key: []
  64
  65  (reg-fx
  66    :http-xhrio

----  Analysis Error : Please see resources/public/js/compiled/out/day8/re_frame/http_fx.cljs  ----

Upgrade ClojureScript to 1.10.520

Tests are broken due to outdated ClojureScript dependency:

Exception in thread "main" java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter, compiling:(cljs/closure.clj:1:1)
	at clojure.lang.Compiler.load(Compiler.java:7391)

compiler warning with clojurescript 1.9.946 -> please bump cljs-ajax version

Version 0.1.4 of re-frame-http-fx

$ lein karma-once
Compiling ClojureScript...
Compiling "resources/public/karma/test.js" from ["test/cljs" "src/cljs"]...
WARNING: uri? already refers to: cljs.core/uri? being replaced by: cognitect.transit/uri? at line 332 resources/public/karma/test/cognitect/transit.cljs
Successfully compiled "resources/public/karma/test.js" in 37.9 seconds.

Version 0.1.5-SNAPSHOT of re-frame-http-fx, after I cloned it and bumped cljs-ajax from 0.5.8 to 0.7.3

$ lein karma-once
Compiling ClojureScript...
Compiling "resources/public/karma/test.js" from ["test/cljs" "src/cljs"]...
Successfully compiled "resources/public/karma/test.js" in 40.251 seconds.

I don't want to make a PR out of this as you might have other issues but happy to do so if you like.

README has an error when posting body

When using this component to post a body (like a form) there is no need to specify a format, as forcefully stated. The implementation ignores it.

This works fine:

(re-frame/reg-event-fx
  :upload-content
  (fn [{:keys [db]} _]
    (let [{:keys [version-name upload-file]} db
          form-data (doto (js/FormData.)
                      (.append "version-name" version-name)
                      (.append "upload-file" upload-file))]
      {:http-xhrio {:method          :post
                    :uri             "http://localhost:3000/api/upload"
                    :body            form-data
                    :timeout         1000
                    :response-format (ajax/json-response-format {:keywords? true})
                    :on-success      [::content-uploaded]
                    :on-failure      [::upload-failed]}})))

The 'Access-Control-Allow-Origin' header contains the invalid value '(null)'

First, thanks for that piece of code. It's well designed and a very handy re-frame effectful effect :-)

However, I've just run into a behaviour I can't explain neither avoid when querying some URL. I've got a minimal (non-)working example to explain stuff rigorously and I describe it in the README.md.

https://github.com/piotr-yuxuan/mwe-cors-unexplained-behaviour

If the error lies on my side and not in your code's (as it's very likely to be) perhaps it would be worth adding a small explanation in your README.md: code needed to reproduce this behaviour is rather simple and I guess a lot of newbie (as I am) would stumble upon this pitfall.

Incremental Processing of Streaming Download?

I've been working out how to get incremental results, not sure if you want to support it as cljs-http might be moving from xhrio (#21). I've got an issue open there, and it can be plugged in at either level.

It's based on me reading through kennethkalmer's PR (#18) and working backwards to figure out where the change should start.

The essence of it is here:

(defn request->xhrio-options
  [{:as   request
    :keys [on-success on-failure on-progress]
    :or   {on-success      [:http-no-on-success]
           on-failure      [:http-no-on-failure]}}]
  ; wrap events in cljs-ajax callback
  (let [api (new js/goog.net.XhrIo)
        _ (when (and on-progress (fn? on-progress))
            (doto api
              (.setProgressEventsEnabled true)
              (events/listen goog.net.EventType.PROGRESS on-progress)))
        ]
    (-> request
        (assoc
          :api     api
          :handler (partial ajax-xhrio-handler
                            #(dispatch (conj on-success %))
                            #(dispatch (conj on-failure %))
                            api))
        (dissoc :on-success :on-failure :on-progress))))

You can now define an on-progress fn that does something with the partial response:

:on-progress
(fn [e]
    (let [resp-text (.getResponseText (.-currentTarget e))]
        resp-text))

Not sure if you want to support it, or should I try upstream?

upgrade to re-frame 1.1.2+ and introduce new fx supporting 2-vector handlers

As of re-frame 1.1.2 there is a new interceptor available unwrap, this is the result of day8/re-frame#644 which promotes 2-vector of [event-id payload-map]

Proposal

  • For backward compatibility, introduce new fx name. This should be fully namespace qualified.
  • Consider if we want to stick with separate :on-success & :on-failure and instead have one callback e.g. :on-completion. This is prompted by recent observations that often the :on-failure path still needs to be engaged while processing a successful result in :on-success (for example, the resulting data fails Spec) and also thinking forward to handlers which may want to use statecharts.
  • Accept an optional second arg (a map) to the :on-xxxx event vec and assoc/merge results to this payload when dispatching.
  • Add optional path to the fx payload/arguments so consumers can control key collisions in their 2-vector response.
  • Better indication of the fx status (don't confuse this with actual HTTP status code) so it is easier for handlers to deal with error, success, timeout etc

Raising this issue to garner opinion or suggestions

Provide default request-format

Same as #4 except for :format instead of :response-format.

If not provided when doing a POST (for example) get a 'unrecognized request format nil' exception.

How do I attach parameters to POST requests?

Resources I looked to for answers:
https://github.com/Day8/re-frame-http-fx
https://github.com/Day8/re-frame-http-fx/blob/master/src/day8/re_frame/http_fx.cljs
https://github.com/Day8/re-frame/blob/master/docs/Talking-To-Servers.md

I mention that as a place where an explanation might be appreciated by fellow devs.

My understanding is that this capacity doesn't exist yet, so I may just need to roll my own, and I'm willing to contribute a PR if you like.

Pass details from request in `on-success` and `on-error`

It's often useful to have things like status codes, headers, e.t.c. even in successful responses, e.g. when getting a 201 with a Location header. Currently we only return the body of the response. There's a few things to think about here:

  • Do we always return the full details, or is it an additional flag?
  • Do we return the raw Xhr object? They are designed to be reused and recycled, so this could be a problem if we ever did this in the future?
  • Do we return a map with :response and :xhr, or extract the useful keys from the Xhr and put them in the map?

Version v0.2.0 release

I noticed that the latest released version is "v0.2.0".

The odd part to this is the prefix "v" in the version string. No previous version had this prefix, eg. "0.1.6".

Was this prefix added intentionally? I wasn't sure if it will cause any issue in typical semantic versioning systems or lexicographical comparisons, etc.

Unable to upload a video to server.

I have the following re-frame handler with which I'm trying to upload a video from a local uri in mobile.

(reg-event-fx
 :upload-shot-video
 (fn [coeffects _]
   (prn "uploading video")
   (let [video {:type "video/mov"
                :uri (-> coeffects :db :shot-video-uri)}
         body (js/FormData.)]
     (prn "uri is " (-> coeffects :db :shot-video-uri))
     (.append body "video" video)
     {:http-xhrio {:method :post
                   :uri (str "http://d18a6571c2e5.ngrok.io" "/api/upload-shot-video")
                   :body body
                   :on-success [:upload-success]
                   :on-failure [:upload-error]
                   :response-format (edn/edn-response-format)}})))

Here's my handler in the server:

(defn upload-shot-video [req]
  (prn "uploading video")
  (prn "video is! " (-> req :params))
  (prn "video is " (-> req :body))
  (clojure.java.io/copy (-> req :body) (clojure.java.io/file "./resources/public/video.mov"))
  (r/response {:res "okay!"}))

Even though an Input stream shows up in (-> req :body), and is saved as a file "./resources/public/video.mov", the file it is saved as is showing up as a file of 0 bytes, suggesting that the data is never sent to the server. How to fix this error?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.