diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -25,9 +25,11 @@
 
 ```
 protoc -I=. --proto_path=./proto \
-  --plugin=protoc-gen-haskell=`which proto-lens-protoc` --haskell_out=./app \
+  --plugin=protoc-gen-haskell=`which proto-lens-protoc`
+  --haskell_out=./app \
   --jsonpb_haskell_out=./app \
-  --plugin=protoc-gen-twirp_haskell=./script/run-twirp_haskell --twirp_haskell_out=./app/Twirp/Example/Haberdasher \
+  --plugin=protoc-gen-twirp_haskell=./script/run-twirp_haskell
+  --twirp_haskell_out=./app/Twirp/Example/Haberdasher \
   haberdasher.proto
 ```
 
diff --git a/src/Twirp/Middleware/Errors.hs b/src/Twirp/Middleware/Errors.hs
--- a/src/Twirp/Middleware/Errors.hs
+++ b/src/Twirp/Middleware/Errors.hs
@@ -1,4 +1,5 @@
-{-# LANGUAGE DerivingVia, DeriveAnyClass, ScopedTypeVariables #-}
+{-# LANGUAGE DerivingVia, DeriveAnyClass, ScopedTypeVariables, ViewPatterns #-}
+-- | Middlewares for handling Twirp error responses.
 module Twirp.Middleware.Errors where
 
 import Data.Aeson
@@ -6,28 +7,42 @@
 import Network.HTTP.Types
 import Network.Wai
 
+import qualified Data.ByteString as BS
+
+-- | A Twirp error that will be sent as a JSON-encoded response body.
+-- See: https://github.com/twitchtv/twirp/blob/master/docs/errors.md
 data TwirpError = TwirpError { code :: String, msg :: String }
   deriving stock (Eq, Show, Generic)
-  deriving anyclass (ToJSON)
+  deriving anyclass (FromJSON, ToJSON)
 
--- Rewrite error responses to use Twirp's error codes and JSON encoding.
--- See: https://github.com/twitchtv/twirp/blob/master/docs/errors.md
+-- | Rewrite error responses to use Twirp's error codes and JSON encoding
+-- when they don't already fit that model.
 twirpErrorResponses :: Middleware
-twirpErrorResponses = modifyResponse go
+twirpErrorResponses = modifyResponse $ \response ->
+  if nonTwirpError response then
+    let
+      status = responseStatus response
+      errResponse err = responseLBS status headers (encode err)
+    in case statusCode status of
+      400 -> errResponse badRequest
+      404 -> errResponse notFound
+      408 -> errResponse canceled
+      500 -> errResponse serverError
+      503 -> errResponse unavailable
+      _   -> errResponse unknown
+  else
+    response
   where
-    go response = let status = responseStatus response in
-      case statusCode status of
-        400 -> responseLBS status headers (encode badRequest)
-        404 -> responseLBS status headers (encode notFound)
-        408 -> responseLBS status headers (encode canceled)
-        500 -> responseLBS status headers (encode serverError)
-        503 -> responseLBS status headers (encode unavailable)
-        _   -> response
+    nonTwirpError r = isError r && not (isJson r)
+    isError (responseStatus -> s) = statusIsClientError s || statusIsServerError s
+    isJson (responseHeaders -> hs) = maybe False ("application/json" `BS.isPrefixOf`) $
+      lookup hContentType hs
 
     headers = [(hContentType, "application/json; charset=utf-8")]
 
     badRequest = TwirpError "invalid_argument" "Bad Request"
     notFound = TwirpError "not_found" "Not found"
     canceled = TwirpError "canceled" "Request Timeout"
-    unavailable = TwirpError "unavailable" "Service Unavailable"
     serverError = TwirpError "internal" "Internal Server Error"
+    unavailable = TwirpError "unavailable" "Service Unavailable"
+    unknown = TwirpError "unknown" "Unknown"
diff --git a/twirp.cabal b/twirp.cabal
--- a/twirp.cabal
+++ b/twirp.cabal
@@ -1,7 +1,7 @@
 cabal-version:  2.4
 
 name:           twirp
-version:        0.2.0.1
+version:        0.2.2.0
 synopsis:       Haskell twirp foundations
 description:    Please see the README on GitHub at <https://github.com/tclem/twirp-haskell#readme>
 homepage:       https://github.com/tclem/twirp-haskell#readme
@@ -15,9 +15,8 @@
 build-type:     Simple
 extra-source-files: README.md
 
-tested-with:    GHC == 8.6.5
-              , GHC == 8.8.1
-              , GHC == 8.10.1
+tested-with:    GHC == 8.10.7
+                GHC == 9.2.2
 
 -- GHC extensions shared between targets
 common haskell
@@ -39,17 +38,17 @@
 
 common dependencies
   build-depends:
-      base >=4.7 && <5
-    , aeson ^>= 1.4.5.0
-    , bytestring >= 0.10.8
-    , http-media >= 0.8.0.0
+      base >= 4.13 && <5
+    , aeson >= 1.4 && < 3
+    , bytestring >= 0.10.8 && < 12
+    , http-media >= 0.8
     , http-types >= 0.12.3
-    , proto-lens >= 0.5.0.0
-    , proto-lens-jsonpb >= 0.2.0.2
-    , proto-lens-runtime ^>= 0.7
-    , servant ^>= 0.18
-    , text ^>= 1.2.3.2
-    , wai ^>= 3.2.2.1
+    , proto-lens >= 0.5 && < 0.8
+    , proto-lens-jsonpb == 0.2.2
+    , proto-lens-runtime
+    , servant >= 0.16 && <0.20
+    , text >= 1.2.3.2 && < 3
+    , wai >= 3.2.2.1
 
 library
   import: haskell, dependencies
