ngx-export-tools-extra 0.3.2.1 → 0.4.0.0
raw patch · 4 files changed
+296/−23 lines, 4 filesdep +binarydep ~http-clientdep ~ngx-export-toolsPVP ok
version bump matches the API change (PVP)
Dependencies added: binary
Dependency ranges changed: http-client, ngx-export-tools
API changes (from Hackage documentation)
+ NgxExport.Tools.Subrequest: extractBodyFromFullResponse :: ByteString -> ByteString
+ NgxExport.Tools.Subrequest: extractHeaderFromFullResponse :: ByteString -> ByteString
+ NgxExport.Tools.Subrequest: extractStatusFromFullResponse :: ByteString -> ByteString
+ NgxExport.Tools.Subrequest: instance GHC.Classes.Eq NgxExport.Tools.Subrequest.ResponseTimeout
+ NgxExport.Tools.Subrequest: instance GHC.Read.Read NgxExport.Tools.Subrequest.ResponseTimeout
+ NgxExport.Tools.Subrequest: makeSubrequestFull :: ByteString -> IO ByteString
+ NgxExport.Tools.Subrequest: makeSubrequestFullWithRead :: ByteString -> IO ByteString
Files
- Changelog.md +5/−0
- NgxExport/Tools/EDE.hs +0/−1
- NgxExport/Tools/Subrequest.hs +287/−19
- ngx-export-tools-extra.cabal +4/−3
Changelog.md view
@@ -1,3 +1,8 @@+### 0.4.0.0++- Implemented adjustable response timeouts.+- Implemented subrequest handlers with ability to extract full response data.+ ### 0.3.2.0 - Functions and handlers *subrequest* and *subrequestWithRead* were renamed to
NgxExport/Tools/EDE.hs view
@@ -71,7 +71,6 @@ -- import NgxExport -- import NgxExport.Tools.EDE ----- import Data.Char -- import Data.ByteString (ByteString) -- import qualified Data.ByteString.Char8 as C8 -- import qualified Data.ByteString.Lazy as L
NgxExport/Tools/Subrequest.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE TemplateHaskell, OverloadedStrings, RecordWildCards #-}-{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeApplications, NumDecimals #-} ----------------------------------------------------------------------------- -- |@@ -22,19 +22,31 @@ -- $makingHTTPSubrequests makeSubrequest ,makeSubrequestWithRead+ -- * Getting full response data from HTTP subrequests+ -- $gettingFullResponse+ ,makeSubrequestFull+ ,makeSubrequestFullWithRead+ ,extractStatusFromFullResponse+ ,extractHeaderFromFullResponse+ ,extractBodyFromFullResponse ) where import NgxExport import NgxExport.Tools -import Network.HTTP.Client+import Network.HTTP.Client hiding (ResponseTimeout)+import qualified Network.HTTP.Client (HttpExceptionContent (ResponseTimeout)) import Network.HTTP.Types import Data.ByteString (ByteString) import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as C8L import qualified Data.Text.Encoding as T-import Data.CaseInsensitive (mk)+import qualified Data.Binary as Binary+import Data.CaseInsensitive (mk, original) import Data.Aeson+import Data.Maybe import Control.Arrow import Control.Exception import System.IO.Unsafe@@ -183,11 +195,15 @@ instance Exception SubrequestParseError +data ResponseTimeout = ResponseTimeoutDefault+ | ResponseTimeout TimeInterval deriving (Eq, Read)+ data SubrequestConf =- SubrequestConf { srMethod :: ByteString- , srUri :: String- , srBody :: ByteString- , srHeaders :: RequestHeaders+ SubrequestConf { srMethod :: ByteString+ , srUri :: String+ , srBody :: ByteString+ , srHeaders :: RequestHeaders+ , srResponseTimeout :: ResponseTimeout } deriving Read instance FromJSON SubrequestConf where@@ -197,11 +213,14 @@ srBody <- maybeEmpty $ o .:? "body" srHeaders <- map (mk . T.encodeUtf8 *** T.encodeUtf8) <$> o .:? "headers" .!= []+ srResponseTimeout <- maybe ResponseTimeoutDefault ResponseTimeout <$>+ o .:? "timeout" return SubrequestConf {..} where maybeEmpty = fmap $ maybe "" T.encodeUtf8 -subrequest :: SubrequestConf -> IO L.ByteString-subrequest SubrequestConf {..} = do+subrequest :: (Response L.ByteString -> L.ByteString) -> SubrequestConf ->+ IO L.ByteString+subrequest fromResponse SubrequestConf {..} = do req <- parseUrlThrow srUri let req' = if B.null srMethod then req@@ -212,8 +231,43 @@ req''' = if null srHeaders then req'' else req'' { requestHeaders = srHeaders }- responseBody <$> httpLbs req''' httpManager+ req'''' = if srResponseTimeout == ResponseTimeoutDefault+ then req'''+ else req''' { responseTimeout =+ setTimeout srResponseTimeout }+ fromResponse <$> httpLbs req'''' httpManager+ where setTimeout (ResponseTimeout v)+ | t == 0 = responseTimeoutNone+ | otherwise = responseTimeoutMicro $ t * 1e6+ where t = toSec v+ setTimeout _ = undefined +subrequestBody :: SubrequestConf -> IO L.ByteString+subrequestBody = subrequest responseBody++type FullResponse = (Int, [(ByteString, L.ByteString)], L.ByteString)++subrequestFull :: SubrequestConf -> IO L.ByteString+subrequestFull = handleAll . subrequest buildResponse+ where handleAll = handle $ \e -> return $ Binary.encode @FullResponse $+ case fromException e of+ Just (HttpExceptionRequest _ c) ->+ case c of+ StatusCodeException r _ ->+ (statusCode $ responseStatus r, [], "")+ Network.HTTP.Client.ResponseTimeout -> response502+ ConnectionTimeout -> response502+ ConnectionFailure _ -> response502+ _ -> response500+ _ -> response500+ response500 = (500, [], "")+ response502 = (502, [], "")+ buildResponse r =+ let status = statusCode $ responseStatus r+ headers = map (first original) $ responseHeaders r+ body = responseBody r+ in Binary.encode (status, headers, body)+ httpManager :: Manager httpManager = unsafePerformIO $ newManager defaultManagerSettings {-# NOINLINE httpManager #-}@@ -226,13 +280,16 @@ -- service handler. -- -- Accepts a JSON object representing an opaque type /SubrequestConf/.--- The object may contain 4 fields: /method/ (optional, default is /GET/),--- /uri/ (mandatory), /body/ (optional, default is an empty value), and--- /headers/ (optional, default is an empty array).+-- The object may contain 5 fields: /method/ (optional, default is /GET/),+-- /uri/ (mandatory), /body/ (optional, default is an empty value), /headers/+-- (optional, default is an empty array), and /timeout/ (optional, default is+-- the default response timeout of the HTTP manager which is normally 30+-- seconds, use value @{\"tag\": \"Unset\"}@ to disable response timeout+-- completely). -- -- Examples of subrequest configurations: ----- > {"uri": "http://example.com/"}+-- > {"uri": "http://example.com/", "timeout": {"tag": "Sec", "contents": 10}} -- -- > {"uri": "http://127.0.0.1/subreq", "method": "POST", "body": "some value"} --@@ -246,8 +303,9 @@ makeSubrequest :: ByteString -- ^ Subrequest configuration -> IO L.ByteString-makeSubrequest = maybe (throwIO SubrequestParseError) subrequest .- readFromByteStringAsJSON @SubrequestConf+makeSubrequest =+ maybe (throwIO SubrequestParseError) subrequestBody .+ readFromByteStringAsJSON @SubrequestConf ngxExportAsyncIOYY 'makeSubrequest @@ -260,9 +318,10 @@ -- An example of a subrequest configuration: -- -- > SubrequestConf { srMethod = ""--- > , srUri = "http://127.0.0.1/subreq"}+-- > , srUri = "http://127.0.0.1/subreq" -- > , srBody = "" -- > , srHeaders = [("Header1", "Value1"), ("Header2", "Value2")]+-- > , srResponseTimeout = ResponseTimeout (Sec 10) -- > } -- -- Notice that unlike JSON parsing, fields of /SubrequestConf/ are not@@ -271,8 +330,217 @@ makeSubrequestWithRead :: ByteString -- ^ Subrequest configuration -> IO L.ByteString-makeSubrequestWithRead = maybe (throwIO SubrequestParseError) subrequest .- readFromByteString @SubrequestConf+makeSubrequestWithRead =+ maybe (throwIO SubrequestParseError) subrequestBody .+ readFromByteString @SubrequestConf ngxExportAsyncIOYY 'makeSubrequestWithRead++-- $gettingFullResponse+--+-- Handlers /makeSubrequest/ and /makeSubrequestWithRead/ return response body+-- of subrequests skipping the response status and headers. To retrieve full+-- data from a response, use another pair of asynchronous variable handlers and+-- functions: __/makeSubrequestFull/__ and __/makeSubrequestFullWithRead/__,+-- and 'makeSubrequestFull' and 'makeSubrequestFullWithRead' respectively.+--+-- Unlike the simple body handlers, there is no sense of using the corresponding+-- variables directly as they are binary encoded values. Instead, the response+-- status, headers and the body must be extracted using handlers+-- __/extractStatusFromFullResponse/__, __/extractHeaderFromFullResponse/__,+-- and __/extractBodyFromFullResponse/__ which are based on functions of the+-- same name.+--+-- Let's extend our example with these handlers.+--+-- File /test_tools_extra_subrequest.hs/ does not have any changes as we are+-- going to use exported handlers only.+--+-- ==== File /nginx.conf/: new location /\/full/ in server /main/+-- @+-- location \/full {+-- haskell_run_async __/makeSubrequestFull/__ $hs_subrequest+-- \'{\"uri\": \"http:\/\/127.0.0.1:$arg_p\/proxy\",+-- \"headers\": [[\"Custom-Header\", \"$arg_a\"]]}\';+--+-- haskell_run __/extractStatusFromFullResponse/__ $hs_subrequest_status+-- $hs_subrequest;+--+-- haskell_run __/extractHeaderFromFullResponse/__ $hs_subrequest_header+-- subrequest-header|$hs_subrequest;+--+-- haskell_run __/extractBodyFromFullResponse/__ $hs_subrequest_body+-- $hs_subrequest;+--+-- if ($hs_subrequest_status = 400) {+-- echo_status 400;+-- echo \"Bad request\";+-- break;+-- }+--+-- if ($hs_subrequest_status = 500) {+-- echo_status 500;+-- echo \"Internal server error while making subrequest\";+-- break;+-- }+--+-- if ($hs_subrequest_status = 502) {+-- echo_status 502;+-- echo \"Backend unavailable\";+-- break;+-- }+--+-- if ($hs_subrequest_status != 200) {+-- echo_status 404;+-- echo \"Subrequest status: $hs_subrequest_status\";+-- break;+-- }+--+-- echo \"Subrequest status: $hs_subrequest_status\";+-- echo \"Subrequest-Header: $hs_subrequest_header\";+-- echo -n \"Subrequest body: $hs_subrequest_body\";+-- }+-- @+--+-- Now we can recognize HTTP response statuses of subrequests and handle them+-- differently. We also can read a response header /Subrequest-Header/.+--+-- ==== File /nginx.conf/: new response header /Subrequest-Header/ in /location \// of server /backend/+-- @+-- add_header Subrequest-Header \"This is response from subrequest\";+-- @+--+-- ==== A simple test+--+-- > $ curl -D- 'http://localhost:8010/full/?a=Value"'+-- > HTTP/1.1 400 Bad Request+-- > Server: nginx/1.17.9+-- > Date: Sat, 04 Apr 2020 12:44:36 GMT+-- > Content-Type: application/octet-stream+-- > Transfer-Encoding: chunked+-- > Connection: keep-alive+-- >+-- > Bad request+--+-- Good. Now we see that adding a comma into a JSON field is a bad request.+--+-- > $ curl -D- 'http://localhost:8010/full/?a=Value'+-- > HTTP/1.1 500 Internal Server Error+-- > Server: nginx/1.17.9+-- > Date: Sat, 04 Apr 2020 12:47:11 GMT+-- > Content-Type: application/octet-stream+-- > Transfer-Encoding: chunked+-- > Connection: keep-alive+-- >+-- > Internal server error while making subrequest+--+-- This is also good. Now we are going to define port of the backend server via+-- argument /$arg_p/. Skipping this makes URI look unparsable+-- (/http:\/\/127.0.0.1:\//) which leads to the error.+--+-- > curl -D- 'http://localhost:8010/full/?a=Value&p=8020'+-- > HTTP/1.1 200 OK+-- > Server: nginx/1.17.9+-- > Date: Sat, 04 Apr 2020 12:52:03 GMT+-- > Content-Type: application/octet-stream+-- > Transfer-Encoding: chunked+-- > Connection: keep-alive+-- >+-- > Subrequest status: 200+-- > Subrequest-Header: This is response from subrequest+-- > Subrequest body: In backend, Custom-Header is 'Value'+--+-- Finally, we are getting a good response with all the response data decoded+-- correctly.+--+-- Let's try another port.+--+-- > curl -D- 'http://localhost:8010/full/?a=Value&p=8021'+-- > HTTP/1.1 502 Bad Gateway+-- > Server: nginx/1.17.9+-- > Date: Sat, 04 Apr 2020 12:56:02 GMT+-- > Content-Type: application/octet-stream+-- > Transfer-Encoding: chunked+-- > Connection: keep-alive+-- >+-- > Backend unavailable+--+-- Good. There is no server listening on port 8021.++-- | Makes an HTTP request.+--+-- The same as 'makeSubrequest' except it returns a binary encoded response data+-- whose parts must be extracted by handlers made of+-- 'extractStatusFromFullResponse', 'extractHeaderFromFullResponse', and+-- 'extractBodyFromFullResponse'.+--+-- Exported on the Nginx level by a handler of the same name.+makeSubrequestFull+ :: ByteString -- ^ Subrequest configuration+ -> IO L.ByteString+makeSubrequestFull =+ maybe (return $ Binary.encode @FullResponse (400, [], "")) subrequestFull .+ readFromByteStringAsJSON @SubrequestConf++ngxExportAsyncIOYY 'makeSubrequestFull++-- | Makes an HTTP request.+--+-- The same as 'makeSubrequestWithRead' except it returns a binary encoded+-- response data whose parts must be extracted by handlers made of+-- 'extractStatusFromFullResponse', 'extractHeaderFromFullResponse', and+-- 'extractBodyFromFullResponse'.+--+-- Exported on the Nginx level by a handler of the same name.+makeSubrequestFullWithRead+ :: ByteString -- ^ Subrequest configuration+ -> IO L.ByteString+makeSubrequestFullWithRead =+ maybe (return $ Binary.encode @FullResponse (400, [], "")) subrequestFull .+ readFromByteString @SubrequestConf++ngxExportAsyncIOYY 'makeSubrequestFullWithRead++-- | Extracts the HTTP status from an encoded response.+--+-- Must be used to extract response data encoded by 'makeSubrequestFull' and+-- 'makeSubrequestFullWithRead'.+--+-- Exported on the Nginx level by a handler of the same name.+extractStatusFromFullResponse :: ByteString -> L.ByteString+extractStatusFromFullResponse = C8L.pack . show .+ (\(a, _, _) -> a) . (Binary.decode @FullResponse) . L.fromStrict++ngxExportYY 'extractStatusFromFullResponse++-- | Extracts a specified header from an encoded response.+--+-- Must be used to extract response data encoded by 'makeSubrequestFull' and+-- 'makeSubrequestFullWithRead'.+--+-- Expects that the encoded response data is attached after the name of the+-- header and a vertical bar such as /Header-Name|$hs_body/. The lookup of the+-- header name is case-insensitive.+--+-- Exported on the Nginx level by a handler of the same name.+extractHeaderFromFullResponse :: ByteString -> L.ByteString+extractHeaderFromFullResponse v =+ let (h, b) = mk *** C8.tail $ C8.break ('|' ==) v+ hs = (\(_, a, _) -> map (first mk) a) $+ Binary.decode @FullResponse $ L.fromStrict b+ in fromMaybe L.empty $ lookup h hs++ngxExportYY 'extractHeaderFromFullResponse++-- | Extracts the body from an encoded response.+--+-- Must be used to extract response data encoded by 'makeSubrequestFull' and+-- 'makeSubrequestFullWithRead'.+--+-- Exported on the Nginx level by a handler of the same name.+extractBodyFromFullResponse :: ByteString -> L.ByteString+extractBodyFromFullResponse =+ (\(_, _, a) -> a) . (Binary.decode @FullResponse) . L.fromStrict++ngxExportYY 'extractBodyFromFullResponse
ngx-export-tools-extra.cabal view
@@ -1,5 +1,5 @@ name: ngx-export-tools-extra-version: 0.3.2.1+version: 0.4.0.0 synopsis: More extra tools for Nginx haskell module description: More extra tools for <https://github.com/lyokha/nginx-haskell-module Nginx haskell module>.@@ -34,10 +34,11 @@ , bytestring >= 0.10.0.0 , base64 >= 0.3.0.0 , ngx-export- , ngx-export-tools >= 0.4.5.0+ , ngx-export-tools >= 0.4.8.0 , aeson >= 1.0.0.0 , http-types >= 0.7.0- , http-client+ , http-client >= 0.5.0+ , binary , case-insensitive , containers , unordered-containers