Using jQuery's Pipe() Method To Change Deferred Resolution
I've really grown to love the Deferred functionality added in jQuery 1.5. Not only does it allow for new kinds of functionality, it also allows existing success and error handlers to be bound in an easier-to-understand, less condensed format. One thing that I never really understood, however, was the pipe() method, added in jQuery 1.6. While I sort of understand the concepts of Decoration and Aspect Oriented Programming (AOP), I was never able to connect that to the use of Deferreds. Then, it occurred to me - maybe I could use the pipe() method to create divergent resolutions from a unified web service response.
When you build a web service, you can choose one of two strategies for returning values. Either you return status codes that mirror your response (what I would recommend); or, you always return a 200 OK response and then let the error data become encapsulated within the response itself (how SOAP responses work). If you are dealing with a 3rd party API that always returns a 200 status code, configuring your AJAX response handlers can be a pain - your "success" handlers have to be used for both success and failed requests.
In such cases, I think we can use the jQuery pipe() method as a way to filter the AJAX responses, piping them through to a more appropriate callback. The pipe() works by allowing either one or both of the promise handlers to be overridden. It can do this by returning an alternate value or, by returning an alternate Promise object.
In the following demo, we're going to call a web service that returns a 200 OK response whether or not the API request was successful. This means that the error data is contained within the "successful" response. As such, we'll use the pipe() method to examine the response; and, if the response data indicates "failure," we'll change its resolution, rerouting it through to the "reject" handlers.
First, let's just take a look at the ColdFusion code that represents our always-fail web service:
web_service.cfm (Our Web Service / API)
<!---
Define the API response. For this demo, we'll always
return a 200 - this will get the JavaScript handler to
parse the response.
--->
<cfset response = {} />
<cfset response[ "success" ] = false />
<cfset response[ "data" ] = javaCast( "null", 0 ) />
<cfset response[ "errors" ] = [ "Your request is not valid." ] />
<!--- Return the response as JSON. --->
<cfcontent
type="application/json"
variable="#toBinary( toBase64( serializeJSON( response ) ) )#"
/>
As you can see, this web service returns a 200 OK status code no matter what; however, if you look at the response object, you'll see that it always indicates a failure. This is the kind of normalized API response that we will denormalize on the client using pipe().
Now, let's take a look at our jQuery code that interacts with this API:
<!DOCTYPE html>
<html>
<head>
<title>Using jQuery's Pipe() To Change Deferred Resolution</title>
<script type="text/javascript" src="../jquery-1.6.3.js"></script>
<script type="text/javascript">
// Make a request to the web service. This API returns a
// standard response structure with the following keys:
//
// - success [boolean]
// - data [any]
// - errors [array]
//
// Capture this request promise so that we can bind
// resolution handlers (fail / success).
var request = $.ajax({
type: "post",
url: "./web_service.cfm",
dataType: "json"
});
// -------------------------------------------------- //
// -------------------------------------------------- //
// Right now, the API response always comes back as a 200
// response code; howver, only some of those responses are
// actually "successful". As such, let's pipe the response
// through a filter in order to change the resolution based
// on the actual response data.
//
// NOTE: We have to override the request variable in order
// have the piping affect the original promise.
request = request.pipe(
// Filter the SUCCESS responses from the API.
function( response ){
// Check to see if the response is truely successful.
if (response.success){
// Since this response is already a success, just
// pass it through to any existing success
// handlers bound to the promise.
return( response );
} else {
// The response is actually a FAIL even though it
// came through as a success (200). Convert this
// promise resolution to a FAIL.
return(
$.Deferred().reject( response )
);
}
},
// Filter the FAIL responses from the API.
function( response ){
// Since the API is designed to always return a
// standardized response, a failure at this point
// means that something really went wrong. As such
// let's just normalize the failure.
return({
success: false,
data: null,
errors: [ "Unexpected error: " + response.status + " " + response.statusText ]
});
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// Now that our API response has been filtered, let's attach
// our success and fail handlers to the promise resolution.
request.then(
function( response ){
// Log the success.
console.log( "Success!!!", response );
},
function( response ){
// Log the failure.
console.log( "Fail!!!", response );
}
);
</script>
</head>
<body>
<!-- Left intentionally blank. -->
</body>
</html>
As you can see, we launch our AJAX request and capture the request promise. Then, we use the pipe() method to override that request promise variable.
The pipe() method provides a filter for both the success and failure resolutions (of the AJAX request). If the original resolution is success, the pipe() filter either passes a truly successful response through; or, it changes the resolution, returning a new rejected promise. Then, if the original request was a failure, which would be truly unexpected in our API, the pipe() filter simply passes through a normalized API response structure.
Using this approach, our local success and failure handlers will be decoupled from the unified 3rd party API response. This allows our local architecture to implement true success and fail callbacks even when the context doesn't readily allow for it. And, when we run the above code, we get the following console output:
Fail!!!
Object { errors=[1], success=false, data=null}
As you can see, our local error handler was invoked even though our 3rd party API returned a 200 OK status code. The pipe() method allowed us to reroute the "success" response based on the encapsulated "fail" data.
This is the first time I've really looked at the pipe() method in jQuery's Deferred architecture; as such, I'm sure there's a lot more to think about. At first glance, I found it hard to come up with any real use-case for it; but, given the many diverse surfaces that an API can present, the pipe() method may be a great way to normalize all responses for your local JavaScript architecture.
Want to use code from this post? Check out the license.
Reader Comments
Nice Ben!. Thanks for sharing the practical use of "pipe".
@Raghav,
Thanks my man. I'd be curious to see how other people use it. I haven't really come across any blog posts in my reader for it.
Just a side note: jQuery claims to be CommonJS Promises/A compliant, but it's not. The .pipe() method wouldn't be necessary if it were, iiuc.
In Promises/A, changing the resolved value is done by returning a promise from a handler in the .then(). I'll let the reader decide which method they like better. :)
The jQuery documentation team should note that they are not really Promises/A compliant. Anybody using any of the other dozens of Promises/A-compliant libraries in conjunction with jQuery.Deferred could toil for hours trying to figure out why their resolved values are becoming undefined or not being changed (depending on the order of the promises).
Nice example, btw, Ben! Perfect use case.
-- John
Hey Ben,
The only time I've used pipe() is to transform a JSON response while caching the promise at the same time. In this particular use case, I had an array of blog posts coming back from a 3rd party server and wanted to add a "summary" property to each blog post. The summary represented the first few sentences of the actual post.
The caching-the-promise part is why I needed to pipe it. I could do the logic in the success callback once and cache the $.ajax promise, but if the post was ever returned from the cache, the filtering logic wouldn't have been applied.
So I used pipe() to produce something like this (pretend only one post is coming back, not an array):
Disclaimer: I wrote this code up quickly; it's untested and is likely riddled with errors, but hopefully it gets the idea across.
Without pipe, I'd have to write the getBlog() function like such:
So now, calling getBlog("foo.xml") guarantees a promise that'll always resolve with the filtered data, regardless if it's cached or not.
Thanks Ben. This is by far the best explanation I have seen of the pipe() method.
@Unscriptable,
It took me like 15 minutes to figure out why my Request wasn't giving me the proper resolution. Then, eventually, I realized that it was because I had to re-capture the return value from the .pipe() method. At first, I thought it would just get hooked up implicitly.
@Eric,
Good stuff; definitely comes across as much cleaner and concise with the .pipe() method. It is pretty cool that the pipe() method can handler and internal return of either a value or a new promise.
@Evagoras,
Thanks!
Nice post, Ben. One minor suggestion, though: $.pipe() returns a promise, while $.ajax returns a jQuery Deferred object. The difference being that a promise does not expose methods that can alter the state of the deferred (resolve(), reject(), resolveWith(), and rejectWith()).
This behavior can be useful in reasoning about your code (returning a promise guarantees that the deferred is completed internally), although it can also make testing more difficult for the same reason.
Hi Mike,
jQuery's AJAX methods actually return a jqXHR object, which includes promise (but not deferred) methods, therefore making it observable but not mutable. All sorts of weird stuff would happen if people were able to resolve/reject their jqXHR objects o_O
- Eric
@Eric,
Whoops--right you are! Guess I spoke too soon there :/
I'll really suggest you this blog and you will see some more usages of pipe http://joseoncode.com/2011/09/26/a-walkthrough-jquery-deferred-and-promise/