allow configuring timeout for remote schema calls (close #2501) (#2753)

This commit is contained in:
Tirumarai Selvan 2019-08-23 04:57:19 -04:00 committed by Vamshi Surabhi
parent 669bb40e72
commit 98784212e2
16 changed files with 218 additions and 55 deletions

View File

@ -103,4 +103,5 @@ if (globals.consoleMode === SERVER_CONSOLE_MODE) {
} }
*/ */
} }
export default globals; export default globals;

View File

@ -21,12 +21,15 @@ const prefixUrl = globals.urlPrefix + appPrefix;
const MANUAL_URL_CHANGED = '@addRemoteSchema/MANUAL_URL_CHANGED'; const MANUAL_URL_CHANGED = '@addRemoteSchema/MANUAL_URL_CHANGED';
const ENV_URL_CHANGED = '@addRemoteSchema/ENV_URL_CHANGED'; const ENV_URL_CHANGED = '@addRemoteSchema/ENV_URL_CHANGED';
const NAME_CHANGED = '@addRemoteSchema/NAME_CHANGED'; const NAME_CHANGED = '@addRemoteSchema/NAME_CHANGED';
const TIMEOUT_CONF_CHANGED = '@addRemoteSchema/TIMEOUT_CONF_CHANGED';
// const HEADER_CHANGED = '@addRemoteSchema/HEADER_CHANGED'; // const HEADER_CHANGED = '@addRemoteSchema/HEADER_CHANGED';
const ADDING_REMOTE_SCHEMA = '@addRemoteSchema/ADDING_REMOTE_SCHEMA'; const ADDING_REMOTE_SCHEMA = '@addRemoteSchema/ADDING_REMOTE_SCHEMA';
const ADD_REMOTE_SCHEMA_FAIL = '@addRemoteSchema/ADD_REMOTE_SCHEMA_FAIL'; const ADD_REMOTE_SCHEMA_FAIL = '@addRemoteSchema/ADD_REMOTE_SCHEMA_FAIL';
const RESET = '@addRemoteSchema/RESET'; const RESET = '@addRemoteSchema/RESET';
const FETCHING_INDIV_REMOTE_SCHEMA = '@addRemoteSchema/FETCHING_INDIV_REMOTE_SCHEMA'; const FETCHING_INDIV_REMOTE_SCHEMA =
const REMOTE_SCHEMA_FETCH_SUCCESS = '@addRemoteSchema/REMOTE_SCHEMA_FETCH_SUCCESS'; '@addRemoteSchema/FETCHING_INDIV_REMOTE_SCHEMA';
const REMOTE_SCHEMA_FETCH_SUCCESS =
'@addRemoteSchema/REMOTE_SCHEMA_FETCH_SUCCESS';
const REMOTE_SCHEMA_FETCH_FAIL = '@addRemoteSchema/REMOTE_SCHEMA_FETCH_FAIL'; const REMOTE_SCHEMA_FETCH_FAIL = '@addRemoteSchema/REMOTE_SCHEMA_FETCH_FAIL';
const DELETING_REMOTE_SCHEMA = '@addRemoteSchema/DELETING_REMOTE_SCHEMA'; const DELETING_REMOTE_SCHEMA = '@addRemoteSchema/DELETING_REMOTE_SCHEMA';
@ -47,6 +50,7 @@ const inputEventMap = {
name: NAME_CHANGED, name: NAME_CHANGED,
envName: ENV_URL_CHANGED, envName: ENV_URL_CHANGED,
manualUrl: MANUAL_URL_CHANGED, manualUrl: MANUAL_URL_CHANGED,
timeoutConf: TIMEOUT_CONF_CHANGED,
}; };
/* Action creators */ /* Action creators */
@ -139,12 +143,17 @@ const addRemoteSchema = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
const currState = getState().remoteSchemas.addData; const currState = getState().remoteSchemas.addData;
// const url = Endpoints.getSchema; // const url = Endpoints.getSchema;
let timeoutSeconds = parseInt(currState.timeoutConf, 10);
if (isNaN(timeoutSeconds)) timeoutSeconds = 60;
const resolveObj = { const resolveObj = {
name: currState.name.trim().replace(/ +/g, ''), name: currState.name.trim().replace(/ +/g, ''),
definition: { definition: {
url: currState.manualUrl, url: currState.manualUrl,
url_from_env: currState.envName, url_from_env: currState.envName,
headers: [], headers: [],
timeout_seconds: timeoutSeconds,
forward_client_headers: currState.forwardClientHeaders, forward_client_headers: currState.forwardClientHeaders,
}, },
}; };
@ -316,11 +325,18 @@ const modifyRemoteSchema = () => {
name: currState.editState.originalName, name: currState.editState.originalName,
}, },
}; };
let newTimeout = parseInt(currState.timeoutConf, 10);
let oldTimeout = parseInt(currState.editState.originalTimeoutConf, 10);
if (isNaN(newTimeout)) newTimeout = 60;
if (isNaN(oldTimeout)) oldTimeout = 60;
const resolveObj = { const resolveObj = {
name: remoteSchemaName, name: remoteSchemaName,
definition: { definition: {
url: currState.manualUrl, url: currState.manualUrl,
url_from_env: currState.envName, url_from_env: currState.envName,
timeout_seconds: newTimeout,
forward_client_headers: currState.forwardClientHeaders, forward_client_headers: currState.forwardClientHeaders,
headers: [], headers: [],
}, },
@ -351,11 +367,13 @@ const modifyRemoteSchema = () => {
name: remoteSchemaName, name: remoteSchemaName,
}, },
}; };
const resolveDownObj = { const resolveDownObj = {
name: currState.editState.originalName, name: currState.editState.originalName,
definition: { definition: {
url: currState.editState.originalUrl, url: currState.editState.originalUrl,
url_from_env: currState.editState.originalEnvUrl, url_from_env: currState.editState.originalEnvUrl,
timeout_seconds: oldTimeout,
headers: [], headers: [],
forward_client_headers: forward_client_headers:
currState.editState.originalForwardClientHeaders, currState.editState.originalForwardClientHeaders,
@ -377,6 +395,7 @@ const modifyRemoteSchema = () => {
...resolveDownObj, ...resolveDownObj,
}, },
}; };
downQueryArgs.push(deleteRemoteSchemaDown); downQueryArgs.push(deleteRemoteSchemaDown);
downQueryArgs.push(createRemoteSchemaDown); downQueryArgs.push(createRemoteSchemaDown);
// End of down // End of down
@ -389,6 +408,7 @@ const modifyRemoteSchema = () => {
type: 'bulk', type: 'bulk',
args: downQueryArgs, args: downQueryArgs,
}; };
const requestMsg = 'Modifying remote schema...'; const requestMsg = 'Modifying remote schema...';
const successMsg = 'Remote schema modified'; const successMsg = 'Remote schema modified';
const errorMsg = 'Modify remote schema failed'; const errorMsg = 'Modify remote schema failed';
@ -441,6 +461,11 @@ const addRemoteSchemaReducer = (state = addState, action) => {
envName: action.data, envName: action.data,
manualUrl: null, manualUrl: null,
}; };
case TIMEOUT_CONF_CHANGED:
return {
...state,
timeoutConf: action.data,
};
case ADDING_REMOTE_SCHEMA: case ADDING_REMOTE_SCHEMA:
return { return {
...state, ...state,
@ -480,6 +505,9 @@ const addRemoteSchemaReducer = (state = addState, action) => {
manualUrl: action.data[0].definition.url || null, manualUrl: action.data[0].definition.url || null,
envName: action.data[0].definition.url_from_env || null, envName: action.data[0].definition.url_from_env || null,
headers: action.data[0].definition.headers || [], headers: action.data[0].definition.headers || [],
timeoutConf: action.data[0].definition.timeout_seconds
? action.data[0].definition.timeout_seconds.toString()
: '60',
forwardClientHeaders: action.data[0].definition.forward_client_headers, forwardClientHeaders: action.data[0].definition.forward_client_headers,
editState: { editState: {
...state, ...state,

View File

@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import globals from '../../../../Globals';
import { REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT } from '../../../../helpers/versionUtils';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip'; import Tooltip from 'react-bootstrap/lib/Tooltip';
@ -36,42 +38,92 @@ class Common extends React.Component {
render() { render() {
const styles = require('../RemoteSchema.scss'); const styles = require('../RemoteSchema.scss');
const { name, manualUrl, envName, forwardClientHeaders } = this.props; const {
name,
manualUrl,
envName,
timeoutConf,
forwardClientHeaders,
} = this.props;
const { isModify, id } = this.props.editState; const { isModify, id } = this.props.editState;
const isDisabled = id >= 0 && !isModify; const isDisabled = id >= 0 && !isModify;
const urlRequired = !manualUrl && !envName; const urlRequired = !manualUrl && !envName;
const graphqlurl = ( const tooltips = {
<Tooltip id="tooltip-cascade"> graphqlurl: (
Remote GraphQL servers URL. E.g. https://my-domain/v1/graphql <Tooltip id="tooltip-cascade">
</Tooltip> Remote GraphQL servers URL. E.g. https://my-domain/v1/graphql
); </Tooltip>
),
clientHeaderForward: (
<Tooltip id="tooltip-cascade">
Toggle forwarding headers sent by the client app in the request to
your remote GraphQL server
</Tooltip>
),
additionalHeaders: (
<Tooltip id="tooltip-cascade">
Custom headers to be sent to the remote GraphQL server
</Tooltip>
),
schema: (
<Tooltip id="tooltip-cascade">
Give this GraphQL schema a friendly name.
</Tooltip>
),
timeoutConf: (
<Tooltip id="tooltip-cascade">
Configure timeout for your remote GraphQL server. Defaults to 60
seconds.
</Tooltip>
),
};
const clientHeaderForward = ( const getTimeoutSection = () => {
<Tooltip id="tooltip-cascade"> const supportTimeoutConf =
Toggle forwarding headers sent by the client app in the request to your globals.featuresCompatibility &&
remote GraphQL server globals.featuresCompatibility[REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT];
</Tooltip>
);
const additionalHeaders = ( if (!supportTimeoutConf) {
<Tooltip id="tooltip-cascade"> return null;
Custom headers to be sent to the remote GraphQL server }
</Tooltip>
);
const schema = ( return (
<Tooltip id="tooltip-cascade"> <React.Fragment>
Give this GraphQL schema a friendly name. <div className={styles.subheading_text}>
</Tooltip> GraphQL server timeout
); <OverlayTrigger placement="right" overlay={tooltips.timeoutConf}>
<i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger>
</div>
<label
className={
styles.inputLabel + ' radio-inline ' + styles.padd_left_remove
}
>
<input
className={'form-control'}
type="text"
placeholder="Timeout in seconds"
value={timeoutConf}
data-key="timeoutConf"
onChange={this.handleInputChange.bind(this)}
disabled={isDisabled}
data-test="remote-schema-timeout-conf"
pattern="^\d+$"
title="Only non negative integers are allowed"
/>
</label>
</React.Fragment>
);
};
return ( return (
<div className={styles.CommonWrapper}> <div className={styles.CommonWrapper}>
<div className={styles.subheading_text + ' ' + styles.addPaddTop}> <div className={styles.subheading_text + ' ' + styles.addPaddTop}>
Remote Schema name * Remote Schema name *
<OverlayTrigger placement="right" overlay={schema}> <OverlayTrigger placement="right" overlay={tooltips.schema}>
<i className="fa fa-question-circle" aria-hidden="true" /> <i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger> </OverlayTrigger>
</div> </div>
@ -95,12 +147,12 @@ class Common extends React.Component {
/> />
</label> </label>
<hr /> <hr />
<h4 className={styles.subheading_text}> <div className={styles.subheading_text}>
GraphQL server URL * GraphQL server URL *
<OverlayTrigger placement="right" overlay={graphqlurl}> <OverlayTrigger placement="right" overlay={tooltips.graphqlurl}>
<i className="fa fa-question-circle" aria-hidden="true" /> <i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger> </OverlayTrigger>
</h4> </div>
<div className={styles.wd_300}> <div className={styles.wd_300}>
<DropdownButton <DropdownButton
dropdownOptions={[ dropdownOptions={[
@ -152,13 +204,19 @@ class Common extends React.Component {
/> />
<span>Forward all headers from client</span> <span>Forward all headers from client</span>
</label> </label>
<OverlayTrigger placement="right" overlay={clientHeaderForward}> <OverlayTrigger
placement="right"
overlay={tooltips.clientHeaderForward}
>
<i className="fa fa-question-circle" aria-hidden="true" /> <i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger> </OverlayTrigger>
</div> </div>
<div className={styles.subheading_text + ' ' + styles.font_normal}> <div className={styles.subheading_text + ' ' + styles.font_normal}>
Additional headers: Additional headers:
<OverlayTrigger placement="right" overlay={additionalHeaders}> <OverlayTrigger
placement="right"
overlay={tooltips.additionalHeaders}
>
<i className="fa fa-question-circle" aria-hidden="true" /> <i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger> </OverlayTrigger>
</div> </div>
@ -174,6 +232,8 @@ class Common extends React.Component {
placeHolderText={this.getPlaceHolderText.bind(this)} placeHolderText={this.getPlaceHolderText.bind(this)}
keyInputPlaceholder="header name" keyInputPlaceholder="header name"
/> />
<hr />
{getTimeoutSection()}
</div> </div>
); );
} }

View File

@ -17,6 +17,7 @@ const addState = {
manualUrl: '', manualUrl: '',
envName: null, envName: null,
headers: [], headers: [],
timeoutConf: '',
name: '', name: '',
forwardClientHeaders: false, forwardClientHeaders: false,
...asyncState, ...asyncState,
@ -27,6 +28,7 @@ const addState = {
originalHeaders: [], originalHeaders: [],
originalUrl: '', originalUrl: '',
originalEnvUrl: '', originalEnvUrl: '',
originalTimeoutConf: '',
originalForwardClientHeaders: false, originalForwardClientHeaders: false,
}, },
}; };

View File

@ -8,6 +8,7 @@ const envObj = `apiHost: '${process.env.API_HOST}',
enableTelemetry: ${process.env.ENABLE_TELEMETRY}, enableTelemetry: ${process.env.ENABLE_TELEMETRY},
assetsPath: '${process.env.ASSETS_PATH}', assetsPath: '${process.env.ASSETS_PATH}',
assetsVersion: '${process.env.ASSETS_VERSION}', assetsVersion: '${process.env.ASSETS_VERSION}',
serverVersion: '${process.env.SERVER_VERSION}',
cdnAssets: ${process.env.CDN_ASSETS}, cdnAssets: ${process.env.CDN_ASSETS},
`; `;

View File

@ -2,12 +2,15 @@ const semver = require('semver');
export const FT_JWT_ANALYZER = 'JWTAnalyzer'; export const FT_JWT_ANALYZER = 'JWTAnalyzer';
export const RELOAD_METADATA_API_CHANGE = 'reloadMetaDataApiChange'; export const RELOAD_METADATA_API_CHANGE = 'reloadMetaDataApiChange';
export const REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT =
'remoteSchemaTimeoutConfSupport';
// list of feature launch versions // list of feature launch versions
const featureLaunchVersions = { const featureLaunchVersions = {
// feature: 'v1.0.0' // feature: 'v1.0.0'
[RELOAD_METADATA_API_CHANGE]: 'v1.0.0-beta.3', [RELOAD_METADATA_API_CHANGE]: 'v1.0.0-beta.3',
[FT_JWT_ANALYZER]: 'v1.0.0-beta.3', [FT_JWT_ANALYZER]: 'v1.0.0-beta.3',
[REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT]: 'v1.0.0-beta.5',
}; };
export const getFeaturesCompatibility = serverVersion => { export const getFeaturesCompatibility = serverVersion => {

View File

@ -30,7 +30,8 @@ An example request as follows:
"definition": { "definition": {
"url": "https://remote-server.com/graphql", "url": "https://remote-server.com/graphql",
"headers": [{"name": "X-Server-Request-From", "value": "Hasura"}], "headers": [{"name": "X-Server-Request-From", "value": "Hasura"}],
"forward_client_headers": false "forward_client_headers": false,
"timeout_seconds": 60
}, },
"comment": "some optional comment" "comment": "some optional comment"
} }

View File

@ -433,7 +433,8 @@ RemoteSchemaDef
"value_from_env": env-var-string "value_from_env": env-var-string
} }
], ],
"forward_client_headers": boolean "forward_client_headers": boolean,
"timeout_seconds": integer
} }
.. _CollectionName: .. _CollectionName:

View File

@ -20,10 +20,10 @@ import Data.Int (Int64)
import Data.IORef (IORef, readIORef) import Data.IORef (IORef, readIORef)
import Data.Time.Clock import Data.Time.Clock
import Hasura.Events.HTTP import Hasura.Events.HTTP
import Hasura.HTTP
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Headers import Hasura.RQL.DDL.Headers
import Hasura.RQL.Types import Hasura.RQL.Types
import Hasura.Server.Version (currentVersion)
import Hasura.SQL.Types import Hasura.SQL.Types
import qualified Control.Concurrent.STM.TQueue as TQ import qualified Control.Concurrent.STM.TQueue as TQ
@ -344,12 +344,6 @@ decodeHeader logenv headerInfos (hdrName, hdrVal)
where where
decodeBS = TE.decodeUtf8With TE.lenientDecode decodeBS = TE.decodeUtf8With TE.lenientDecode
addDefaultHeaders :: [HTTP.Header] -> [HTTP.Header]
addDefaultHeaders hdrs = hdrs ++
[ (CI.mk "Content-Type", "application/json")
, (CI.mk "User-Agent", "hasura-graphql-engine/" <> T.encodeUtf8 currentVersion)
]
mkInvo mkInvo
:: EventPayload -> Int -> [HeaderConf] -> TBS.TByteString -> [HeaderConf] :: EventPayload -> Int -> [HeaderConf] -> TBS.TByteString -> [HeaderConf]
-> Invocation -> Invocation

View File

@ -371,19 +371,26 @@ execRemoteGQ reqId userInfo reqHdrs q rsi opDef = do
, Map.fromList userInfoToHdrs , Map.fromList userInfoToHdrs
, Map.fromList clientHdrs , Map.fromList clientHdrs
] ]
finalHdrs = foldr Map.union Map.empty hdrMaps headers = Map.toList $ foldr Map.union Map.empty hdrMaps
options = wreqOptions manager (Map.toList finalHdrs) finalHeaders = addDefaultHeaders headers
initReqE <- liftIO $ try $ HTTP.parseRequest (show url)
initReq <- either httpThrow pure initReqE
let req = initReq
{ HTTP.method = "POST"
, HTTP.requestHeaders = finalHeaders
, HTTP.requestBody = HTTP.RequestBodyLBS (J.encode q)
, HTTP.responseTimeout = HTTP.responseTimeoutMicro (timeout * 1000000)
}
-- log the graphql query
liftIO $ logGraphqlQuery logger $ QueryLog q Nothing reqId liftIO $ logGraphqlQuery logger $ QueryLog q Nothing reqId
res <- liftIO $ try $ Wreq.postWith options (show url) (J.toJSON q) res <- liftIO $ try $ HTTP.httpLbs req manager
resp <- either httpThrow return res resp <- either httpThrow return res
let cookieHdrs = getCookieHdr (resp ^.. Wreq.responseHeader "Set-Cookie") let cookieHdrs = getCookieHdr (resp ^.. Wreq.responseHeader "Set-Cookie")
respHdrs = Just $ mkRespHeaders cookieHdrs respHdrs = Just $ mkRespHeaders cookieHdrs
return $ HttpResponse (encJFromLBS $ resp ^. Wreq.responseBody) respHdrs return $ HttpResponse (encJFromLBS $ resp ^. Wreq.responseBody) respHdrs
where where
RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi RemoteSchemaInfo url hdrConf fwdClientHdrs timeout = rsi
httpThrow :: (MonadError QErr m) => HTTP.HttpException -> m a httpThrow :: (MonadError QErr m) => HTTP.HttpException -> m a
httpThrow = \case httpThrow = \case
HTTP.HttpExceptionRequest _req content -> throw500 $ T.pack . show $ content HTTP.HttpExceptionRequest _req content -> throw500 $ T.pack . show $ content

View File

@ -5,6 +5,7 @@ import Control.Lens ((^.))
import Data.Aeson ((.:), (.:?)) import Data.Aeson ((.:), (.:?))
import Data.FileEmbed (embedStringFile) import Data.FileEmbed (embedStringFile)
import Data.Foldable (foldlM) import Data.Foldable (foldlM)
import Hasura.HTTP
import Hasura.Prelude import Hasura.Prelude
import qualified Data.Aeson as J import qualified Data.Aeson as J
@ -19,7 +20,6 @@ import qualified Language.GraphQL.Draft.Syntax as G
import qualified Network.HTTP.Client as HTTP import qualified Network.HTTP.Client as HTTP
import qualified Network.Wreq as Wreq import qualified Network.Wreq as Wreq
import Hasura.HTTP (wreqOptions)
import Hasura.RQL.DDL.Headers (getHeadersFromConf) import Hasura.RQL.DDL.Headers (getHeadersFromConf)
import Hasura.RQL.Types import Hasura.RQL.Types
import Hasura.Server.Utils (httpExceptToJSON) import Hasura.Server.Utils (httpExceptToJSON)
@ -37,12 +37,21 @@ fetchRemoteSchema
-> RemoteSchemaName -> RemoteSchemaName
-> RemoteSchemaInfo -> RemoteSchemaInfo
-> m GC.RemoteGCtx -> m GC.RemoteGCtx
fetchRemoteSchema manager name def@(RemoteSchemaInfo url headerConf _) = do fetchRemoteSchema manager name def@(RemoteSchemaInfo url headerConf _ timeout) = do
headers <- getHeadersFromConf headerConf headers <- getHeadersFromConf headerConf
let hdrs = flip map headers $ let hdrs = flip map headers $
\(hn, hv) -> (CI.mk . T.encodeUtf8 $ hn, T.encodeUtf8 hv) \(hn, hv) -> (CI.mk . T.encodeUtf8 $ hn, T.encodeUtf8 hv)
options = wreqOptions manager hdrs hdrsWithDefaults = addDefaultHeaders hdrs
res <- liftIO $ try $ Wreq.postWith options (show url) introspectionQuery
initReqE <- liftIO $ try $ HTTP.parseRequest (show url)
initReq <- either throwHttpErr pure initReqE
let req = initReq
{ HTTP.method = "POST"
, HTTP.requestHeaders = hdrsWithDefaults
, HTTP.requestBody = HTTP.RequestBodyLBS introspectionQuery
, HTTP.responseTimeout = HTTP.responseTimeoutMicro (timeout * 1000000)
}
res <- liftIO $ try $ HTTP.httpLbs req manager
resp <- either throwHttpErr return res resp <- either throwHttpErr return res
let respData = resp ^. Wreq.responseBody let respData = resp ^. Wreq.responseBody

View File

@ -2,6 +2,7 @@ module Hasura.HTTP
( wreqOptions ( wreqOptions
, HttpException(..) , HttpException(..)
, hdrsToText , hdrsToText
, addDefaultHeaders
) where ) where
import Control.Lens hiding ((.=)) import Control.Lens hiding ((.=))
@ -25,9 +26,21 @@ hdrsToText hdrs =
wreqOptions :: HTTP.Manager -> [HTTP.Header] -> Wreq.Options wreqOptions :: HTTP.Manager -> [HTTP.Header] -> Wreq.Options
wreqOptions manager hdrs = wreqOptions manager hdrs =
Wreq.defaults Wreq.defaults
& Wreq.headers .~ contentType : userAgent : hdrs & Wreq.headers .~ addDefaultHeaders hdrs
& Wreq.checkResponse ?~ (\_ _ -> return ()) & Wreq.checkResponse ?~ (\_ _ -> return ())
& Wreq.manager .~ Right manager & Wreq.manager .~ Right manager
-- Adds defaults headers overwriting any existing ones
addDefaultHeaders :: [HTTP.Header] -> [HTTP.Header]
addDefaultHeaders hdrs = defaultHeaders <> rmDefaultHeaders hdrs
where
rmDefaultHeaders = filter (not . isDefaultHeader)
isDefaultHeader :: HTTP.Header -> Bool
isDefaultHeader (hdrName, _) = hdrName `elem` (map fst defaultHeaders)
defaultHeaders :: [HTTP.Header]
defaultHeaders = [contentType, userAgent]
where where
contentType = ("Content-Type", "application/json") contentType = ("Content-Type", "application/json")
userAgent = ( "User-Agent" userAgent = ( "User-Agent"

View File

@ -30,6 +30,7 @@ data RemoteSchemaInfo
{ rsUrl :: !N.URI { rsUrl :: !N.URI
, rsHeaders :: ![HeaderConf] , rsHeaders :: ![HeaderConf]
, rsFwdClientHeaders :: !Bool , rsFwdClientHeaders :: !Bool
, rsTimeoutSeconds :: !Int
} deriving (Show, Eq, Lift, Generic) } deriving (Show, Eq, Lift, Generic)
instance Hashable RemoteSchemaInfo instance Hashable RemoteSchemaInfo
@ -42,6 +43,7 @@ data RemoteSchemaDef
, _rsdUrlFromEnv :: !(Maybe UrlFromEnv) , _rsdUrlFromEnv :: !(Maybe UrlFromEnv)
, _rsdHeaders :: !(Maybe [HeaderConf]) , _rsdHeaders :: !(Maybe [HeaderConf])
, _rsdForwardClientHeaders :: !Bool , _rsdForwardClientHeaders :: !Bool
, _rsdTimeoutSeconds :: !(Maybe Int)
} deriving (Show, Eq, Lift) } deriving (Show, Eq, Lift)
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase) ''RemoteSchemaDef) $(J.deriveJSON (J.aesonDrop 4 J.snakeCase) ''RemoteSchemaDef)
@ -77,15 +79,18 @@ validateRemoteSchemaDef
:: (MonadError QErr m, MonadIO m) :: (MonadError QErr m, MonadIO m)
=> RemoteSchemaDef => RemoteSchemaDef
-> m RemoteSchemaInfo -> m RemoteSchemaInfo
validateRemoteSchemaDef (RemoteSchemaDef mUrl mUrlEnv hdrC fwdHdrs) = validateRemoteSchemaDef (RemoteSchemaDef mUrl mUrlEnv hdrC fwdHdrs mTimeout) =
case (mUrl, mUrlEnv) of case (mUrl, mUrlEnv) of
(Just url, Nothing) -> (Just url, Nothing) ->
return $ RemoteSchemaInfo url hdrs fwdHdrs return $ RemoteSchemaInfo url hdrs fwdHdrs timeout
(Nothing, Just urlEnv) -> do (Nothing, Just urlEnv) -> do
url <- getUrlFromEnv urlEnv url <- getUrlFromEnv urlEnv
return $ RemoteSchemaInfo url hdrs fwdHdrs return $ RemoteSchemaInfo url hdrs fwdHdrs timeout
(Nothing, Nothing) -> (Nothing, Nothing) ->
throw400 InvalidParams "both `url` and `url_from_env` can't be empty" throw400 InvalidParams "both `url` and `url_from_env` can't be empty"
(Just _, Just _) -> (Just _, Just _) ->
throw400 InvalidParams "both `url` and `url_from_env` can't be present" throw400 InvalidParams "both `url` and `url_from_env` can't be present"
where hdrs = fromMaybe [] hdrC where
hdrs = fromMaybe [] hdrC
timeout = fromMaybe 60 mTimeout

View File

@ -10,6 +10,8 @@ from webserver import RequestHandler, WebServer, MkHandlers, Response
from enum import Enum from enum import Enum
import time
def mkJSONResp(graphql_result): def mkJSONResp(graphql_result):
return Response(HTTPStatus.OK, graphql_result.to_dict(), return Response(HTTPStatus.OK, graphql_result.to_dict(),
{'Content-Type': 'application/json'}) {'Content-Type': 'application/json'})
@ -25,9 +27,15 @@ class HelloWorldHandler(RequestHandler):
class Hello(graphene.ObjectType): class Hello(graphene.ObjectType):
hello = graphene.String(arg=graphene.String(default_value="world")) hello = graphene.String(arg=graphene.String(default_value="world"))
delayedHello = graphene.String(arg=graphene.String(default_value="world"))
def resolve_hello(self, info, arg): def resolve_hello(self, info, arg):
return "Hello " + arg return "Hello " + arg
def resolve_delayedHello(self, info, arg):
time.sleep(10)
return "Hello " + arg
hello_schema = graphene.Schema(query=Hello, subscription=Hello) hello_schema = graphene.Schema(query=Hello, subscription=Hello)
class HelloGraphQL(RequestHandler): class HelloGraphQL(RequestHandler):

View File

@ -0,0 +1,12 @@
description: Simple GraphQL query (takes 10s to respond)
url: /v1/graphql
status: 200
response:
errors:
- message: ResponseTimeout
extensions: {'path': '$', 'code': 'unexpected'}
query:
query: |
query {
delayedHello(arg: "me")
}

View File

@ -6,13 +6,14 @@ import yaml
import json import json
import queue import queue
import requests import requests
import time
import pytest import pytest
from validate import check_query_f, check_query from validate import check_query_f, check_query
def mk_add_remote_q(name, url, headers=None, client_hdrs=False): def mk_add_remote_q(name, url, headers=None, client_hdrs=False, timeout=None):
return { return {
"type": "add_remote_schema", "type": "add_remote_schema",
"args": { "args": {
@ -21,7 +22,8 @@ def mk_add_remote_q(name, url, headers=None, client_hdrs=False):
"definition": { "definition": {
"url": url, "url": url,
"headers": headers, "headers": headers,
"forward_client_headers": client_hdrs "forward_client_headers": client_hdrs,
"timeout_seconds": timeout
} }
} }
} }
@ -434,6 +436,22 @@ class TestAddRemoteSchemaCompareRootQueryFields:
compare_flds(fldH, fldR) compare_flds(fldH, fldR)
assert has_fld[fldR['name']], 'Field ' + fldR['name'] + ' in the remote shema root query type not found in Hasura schema' assert has_fld[fldR['name']], 'Field ' + fldR['name'] + ' in the remote shema root query type not found in Hasura schema'
class TestRemoteSchemaTimeout:
dir = 'queries/remote_schemas'
teardown = {"type": "clear_metadata", "args": {}}
@pytest.fixture(autouse=True)
def transact(self, hge_ctx):
q = mk_add_remote_q('simple 1', 'http://localhost:5000/hello-graphql', timeout = 5)
st_code, resp = hge_ctx.v1q(q)
assert st_code == 200, resp
yield
hge_ctx.v1q(self.teardown)
def test_remote_query_timeout(self, hge_ctx):
check_query_f(hge_ctx, self.dir + '/basic_timeout_query.yaml')
# wait for graphql server to finish else teardown throws
time.sleep(6)
# def test_remote_query_variables(self, hge_ctx): # def test_remote_query_variables(self, hge_ctx):
# pass # pass