{"id":6503,"date":"2022-05-31T08:30:02","date_gmt":"2022-05-31T15:30:02","guid":{"rendered":"https:\/\/cloudfour.com\/?p=6503"},"modified":"2022-05-27T11:09:44","modified_gmt":"2022-05-27T18:09:44","slug":"defensive-api-handling","status":"publish","type":"post","link":"https:\/\/cloudfour.com\/thinks\/defensive-api-handling\/","title":{"rendered":"Defensive API Handling"},"content":{"rendered":"<div class=\"u-pullSides1 u-md-pullSides6\"><figure class=\"Figure\"><img src=\"https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone.png\" alt=\"\" width=\"1600\" height=\"900\" class=\"size-full wp-image-6506\" srcset=\"https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone.png 1600w, https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone-300x169.png 300w, https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone-1024x576.png 1024w, https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone-768x432.png 768w, https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone-1536x864.png 1536w\" sizes=\"(max-width: 1600px) 100vw, 1600px\" \/><\/figure><\/div>\n<div class=\"u-bgGray u-pad1 u-pullSides1 u-spaceItems1 u-textGrow1\"><p>On a recent client project, we had a form that submitted to a third-party registration service. They sent us some documentation for the API, and we built the form. Easy-peasy, right? What followed was a comical series of incidents that served as an excellent lesson in defensive API handling.<\/p><\/div>\n<p>I\u2019d like to walk you through a set of safety checks you can add to an API connection to make it more resilient. Some of the scenarios I describe may feel like edge cases, but they\u2019re all based on real-world situations we\u2019ve encountered. Any of them could cause an app like our registration form to fail in a way that looked to the user like the website was broken\u2026 or worse, as if their registration succeeded when it really failed.<\/p>\n<h2>A basic API post<\/h2>\n<p>Here\u2019s the most basic version of our registration function. It makes a POST request to the registration API using the <code>fetch()<\/code> method and then decodes the API\u2019s JSON response into a JavaScript object that we can return.<\/p>\n<pre aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> registrationV1 = <span class=\"hljs-keyword\">async<\/span> (userObject) =&gt; {\n  <span class=\"hljs-comment\">\/\/ use fetch to post the user data to the registration API<\/span>\n  <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-string\">\"https:\/\/example.com\/register\"<\/span>, {\n    <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">\"POST\"<\/span>,\n    <span class=\"hljs-attr\">headers<\/span>: { <span class=\"hljs-string\">\"Content-Type\"<\/span>: <span class=\"hljs-string\">\"application\/json;charset=utf-8\"<\/span> },\n    <span class=\"hljs-attr\">body<\/span>: <span class=\"hljs-built_in\">JSON<\/span>.stringify(userObject),\n  });\n\n  <span class=\"hljs-comment\">\/\/ decode the response<\/span>\n  <span class=\"hljs-keyword\">const<\/span> json = <span class=\"hljs-keyword\">await<\/span> response.json();\n\n  <span class=\"hljs-comment\">\/\/ return the response object from the API<\/span>\n  <span class=\"hljs-keyword\">return<\/span> json;\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h2>Check if the API response is okay<\/h2>\n<p>That code works fine if your API server is reliable. But something that always gets me about the <code>fetch()<\/code> method is that it only fails if the server is completely offline. That is, if the server responds with a status code that indicates an error, <code>fetch()<\/code> considers that a successful connection, and will happily pass the error response on.<\/p>\n<p>What kind of errors might you receive from an API server? A few we\u2019ve run into include 403 and 503. A 403 error might mean we didn\u2019t pass the proper credentials, such as an access token. A 503 error might be returned if the API service is unavailable, but the server is online. In both cases, the <code>fetch()<\/code> request succeeded (at returning the error it received).<\/p>\n<p>If that happened, our code would return the API response containing the server error, and our application wouldn\u2019t know what to do with it. Luckily, we can address it pretty easily by checking if the response includes the <code>ok<\/code> property. That\u2019s a <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Response\/ok\">shorthand code for \u201csuccessful response,\u201d<\/a> and it\u2019s <code>true<\/code> if the status code from the server was in the 200 range.<\/p>\n<pre aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> registrationV2 = <span class=\"hljs-keyword\">async<\/span> (userObject) =&gt; {\n  <span class=\"hljs-comment\">\/\/ use fetch to post the user data to the registration API<\/span>\n  <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(url, options);\n\n  <span class=\"hljs-comment\">\/\/ check if the response is successful before proceeding<\/span>\n  <span class=\"hljs-keyword\">if<\/span> (!response.ok) <span class=\"hljs-keyword\">return<\/span>;\n\n  <span class=\"hljs-comment\">\/\/ decode the response<\/span>\n  <span class=\"hljs-keyword\">const<\/span> json = <span class=\"hljs-keyword\">await<\/span> response.json();\n\n  <span class=\"hljs-comment\">\/\/ return the response object from the API<\/span>\n  <span class=\"hljs-keyword\">return<\/span> json;\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h2>Handle errors<\/h2>\n<p>At this point, there are two places in our code where JavaScript might throw an error \u2014 if the initial <code>fetch()<\/code> request fails, or if the <code>json()<\/code> decoding step fails. As we discussed, the <code>fetch()<\/code> request will only fail if the API is completely unavailable. Why would the JSON decoding step fail?<\/p>\n<p>Well, imagine a misconfigured API experiencing a server error, but rather than returning a 500 status code, it returns a 200 status code, and the body of the response is the HTML contents of the server\u2019s error page. Our code sees a 200, and passes the response on to <code>json()<\/code> to be decoded, but it fails because the response is not JSON!<\/p>\n<p>If that happens, our script will stop executing and the page will likely break. Another bad experience for our users. But since we know this might happen, we can use a <code>try...catch<\/code> statement. Then if anything in our code throws an error, it will be passed to our <code>catch<\/code> block, where we can handle it in a way that doesn\u2019t break our app.<\/p>\n<pre aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> registrationV3 = <span class=\"hljs-keyword\">async<\/span> (userObject) =&gt; {\n  <span class=\"hljs-comment\">\/\/ use a try...catch to handle any errors that might occur<\/span>\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-comment\">\/\/ use fetch to post the user data to the registration API<\/span>\n    <span class=\"hljs-comment\">\/\/ (will throw an error if the url does not respond)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(url, options);\n\n    <span class=\"hljs-comment\">\/\/ check if the response is successful before proceeding<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (!response.ok) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`Response: <span class=\"hljs-subst\">${response.status}<\/span>`<\/span>);\n\n    <span class=\"hljs-comment\">\/\/ try to decode the response<\/span>\n    <span class=\"hljs-comment\">\/\/ (will throw an error if JSON can't be parsed)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> json = <span class=\"hljs-keyword\">await<\/span> response.json();\n\n    <span class=\"hljs-comment\">\/\/ return the response object from the API<\/span>\n    <span class=\"hljs-keyword\">return<\/span> json;\n\n  } <span class=\"hljs-keyword\">catch<\/span> (err) {\n    <span class=\"hljs-comment\">\/\/ trigger some code to display an error to the user<\/span>\n    <span class=\"hljs-built_in\">console<\/span>.error(err.message);\n  }\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Note that we\u2019ve also gone back and changed our <code>response.ok<\/code> check to throw an error instead of returning. So we now have three possible error states covered!<\/p>\n<h2>Check if the response is okay, but contains an error<\/h2>\n<p>Now, if something has gone wrong with the API, it <em>should<\/em> return an appropriate response status. A response in the 200s <em>should<\/em> mean everything is good. However, it\u2019s distressingly common for poorly-written APIs to return a 200 status code, even if the JSON itself contains an error.<\/p>\n<p>This might happen if there\u2019s no server error, but something with the request went wrong. Maybe the server couldn\u2019t save our registration. Or maybe that user has already registered! There are status codes to cover these scenarios, but many APIs will return a 200 and a JSON response with an <code>error<\/code> or <code>errors<\/code> property.<\/p>\n<p>We can handle this situation by adding another safety check to our code. If we find either of these properties in the response, we can throw an error.<\/p>\n<pre aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> registrationV4 = <span class=\"hljs-keyword\">async<\/span> (userObject) =&gt; {\n  <span class=\"hljs-comment\">\/\/ use a try...catch to handle any errors that might occur<\/span>\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-comment\">\/\/ use fetch to post the user data to the registration API<\/span>\n    <span class=\"hljs-comment\">\/\/ (will throw an error if the url does not respond)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(url, options);\n\n    <span class=\"hljs-comment\">\/\/ check if the response is successful before proceeding<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (!response.ok) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`Response: <span class=\"hljs-subst\">${response.status}<\/span>`<\/span>);\n\n    <span class=\"hljs-comment\">\/\/ try to decode the response<\/span>\n    <span class=\"hljs-comment\">\/\/ (will throw an error if JSON can't be parsed)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> json = <span class=\"hljs-keyword\">await<\/span> response.json();\n\n    <span class=\"hljs-comment\">\/\/ check if the response contains an `error` or `errors` property<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (json.error) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(json.error);\n    <span class=\"hljs-keyword\">if<\/span> (json.errors) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-built_in\">JSON<\/span>.stringify(json.errors));\n\n    <span class=\"hljs-comment\">\/\/ return the response object from the API<\/span>\n    <span class=\"hljs-keyword\">return<\/span> json;\n\n  } <span class=\"hljs-keyword\">catch<\/span> (err) {\n    <span class=\"hljs-comment\">\/\/ trigger some code to display an error to the user<\/span>\n    <span class=\"hljs-built_in\">console<\/span>.error(err.message);\n  }\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><em>Note: Don\u2019t assume your API sends the same properties. Check what it sends when there\u2019s an error, and update this check to match!<\/em><\/p>\n<p>In this case, our API documentation told us that it will either return an <code>error<\/code> property, which contains a string, or it will return an <code>errors<\/code> array. I don\u2019t know the structure of the <code>errors<\/code> array, so I\u2019m JSON encoding it since <code>Error()<\/code> expects to be passed a string.<\/p>\n<h2>Check if the response is what we\u2019re expecting<\/h2>\n<p>Now, there\u2019s just one final situation that we\u2019re going to check for. Sometimes we get a successful response, and it doesn\u2019t contain an error, but it also doesn\u2019t contain what we\u2019re expecting. For example, an endpoint meant to update a single record could easily be misconfigured to return multiple records, which would be terrible for security and performance.<\/p>\n<p>As a result, it makes sense to check if the response contains the properties we were expecting.<\/p>\n<pre aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> registrationV5 = <span class=\"hljs-keyword\">async<\/span> (userObject) =&gt; {\n  <span class=\"hljs-comment\">\/\/ use a try...catch to handle any errors that might occur<\/span>\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-comment\">\/\/ use fetch to post the user data to the registration API<\/span>\n    <span class=\"hljs-comment\">\/\/ (will throw an error if the url does not respond)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(url, options);\n\n    <span class=\"hljs-comment\">\/\/ check if the response is successful before proceeding<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (!response.ok) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`Response: <span class=\"hljs-subst\">${response.status}<\/span>`<\/span>);\n\n    <span class=\"hljs-comment\">\/\/ try to decode the response<\/span>\n    <span class=\"hljs-comment\">\/\/ (will throw an error if JSON can't be parsed)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> json = <span class=\"hljs-keyword\">await<\/span> response.json();\n\n    <span class=\"hljs-comment\">\/\/ check if the response contains an `error` or `errors` property<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (json.error) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(json.error);\n    <span class=\"hljs-keyword\">if<\/span> (json.errors) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-built_in\">JSON<\/span>.stringify(json.errors));\n\n    <span class=\"hljs-comment\">\/\/ validate the shape of the response<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (!(json.count &amp;&amp; json.results))\n      <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"The API returned an unexpected response.\"<\/span>);\n\n    <span class=\"hljs-comment\">\/\/ return the response object from the API<\/span>\n    <span class=\"hljs-keyword\">return<\/span> json;\n\n  } <span class=\"hljs-keyword\">catch<\/span> (err) {\n    <span class=\"hljs-comment\">\/\/ trigger some code to display an error to the user<\/span>\n    <span class=\"hljs-built_in\">console<\/span>.error(err.message);\n  }\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><em>Note: The <code>json.count<\/code> and <code>json.results<\/code> properties are just examples. You should replace them with properties that your API returns.<\/em><\/p>\n<h2>Conclusion<\/h2>\n<p>To be clear, we\u2019ve run into all of the following problems in real-world situations:<\/p>\n<ul>\n<li>The API does not respond.<\/li>\n<li>The API responds with a 400 or 500 range status code.<\/li>\n<li>The API responds with a 200 but the response is not JSON.<\/li>\n<li>The API responds with a 200 but the response has an <code>error<\/code> or <code>errors<\/code> property.<\/li>\n<li>The API responds with a 200 but the response contains unexpected content.<\/li>\n<\/ul>\n<p>When a user tries to register and one of these happens, we don\u2019t want the app to crash or (worse) look like things succeeded when they really failed. This code lets us catch those scenarios and display a helpful message to the user.<\/p>\n<p>In a perfect world, we wouldn\u2019t need all these safety checks. Ideally, all APIs would respect the standards and respond with an appropriate status code. But we don\u2019t live in a perfect world, and unfortunately, when an API behaves unexpectedly, it\u2019s your users who pay the price.<\/p>\n<p>So even if it feels like a bit much, it\u2019s wise to add safety checks to validate we\u2019re receiving what we expect from an API while handling any errors we might encounter along the way.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>On a recent client project, we built a form that submitted to a third-party registration service. Easy-peasy, right? What followed was a comical series of incidents that served as an excellent lesson in defensive API handling.<\/p>\n","protected":false},"author":21,"featured_media":6506,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[256,240],"tags":[305,307,308,309,306],"class_list":["post-6503","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development","category-javascript","tag-apis","tag-errors","tag-fetch","tag-json","tag-safety"],"acf":[],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/cloudfour.com\/wp-content\/uploads\/2022\/05\/cone.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/posts\/6503","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/comments?post=6503"}],"version-history":[{"count":0,"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/posts\/6503\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/media\/6506"}],"wp:attachment":[{"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/media?parent=6503"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/categories?post=6503"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudfour.com\/wp-json\/wp\/v2\/tags?post=6503"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}