diff --git a/cypress/e2e/http_block_page_following.ts b/cypress/e2e/http_block.ts similarity index 72% rename from cypress/e2e/http_block_page_following.ts rename to cypress/e2e/http_block.ts index 4bbac1c49..d220b2d19 100644 --- a/cypress/e2e/http_block_page_following.ts +++ b/cypress/e2e/http_block.ts @@ -3,7 +3,101 @@ import { loadFlowCode } from '../support/helper'; // tslint:disable: quotemark /// -describe('HTTP Block Integration Tests', () => { + +describe('HTTP Block Request', () => { + + beforeEach(() => { + // Prevent external network request for adapter config + cy.intercept('GET', 'https://kendraio.github.io/kendraio-adapter/config.json', { + fixture: 'adapterConfig.json' + }); + + // Prevent external network requests for Workflow cloud + cy.intercept('GET', 'https://app.kendra.io/api/workflowCloud/listWorkflows', { + fixture: 'workflow-cloud.json' + }); + + // Prevent external network requests for fonts with empty CSS rule + cy.intercept('https://fonts.googleapis.com/\*\*', "\*{ }"); + }); + + it('should return a single set of results. Without pagination', () => { + cy.intercept({ + url: 'https://example.com/data' + }, { + statusCode: 200, + body: '["hippo", "giraffe"]' + }); + + loadFlowCode([ + { "type": "init" }, + { + "type": "http", + "method": "GET", + "endpoint": "https://example.com/data" + }, + { + "type": "debug", + "open": 2, + "showData": true + } + ]); + cy.contains('hippo'); + cy.contains('giraffe'); + }); + + it('should return an error', () => { + cy.intercept({ + url: 'https://example.com/data' + }, { + statusCode: 400, + body: { error: { + error: "Http failure 400 Bad request", + error_description: "There was a problem with your request" + } + } + }); + + loadFlowCode([ + { "type": "init" }, + { + "type": "http", + "method": "GET", + "endpoint": "https://example.com/data", + "onError": { + "blocks": [ + { + "type": "card", + "blocks": [ + { + "type": "template", + "template": "Error with submission:

{{data.error.error}} - {{data.error.error_description}}

" + } + ] + } + ] + } + + }, + { + "type": "debug", + "open": 3, + "showData": true + } + ]); + + cy.contains('hasError:true'); + cy.contains('status:400'); + cy.contains('errorMessage:"Http failure response for https://example.com/data: 400 Bad Request"'); + cy.get('app-template-block').contains('Error with submission') + }); + + +}); + + + +describe('HTTP Block Follow Pagination', () => { beforeEach(() => { // Prevent external network request for adapter config @@ -119,31 +213,6 @@ describe('HTTP Block Integration Tests', () => { cy.contains('birds'); }); - it('should return a single set of results if response is not paginated', () => { - cy.intercept({ - url: 'https://example.com/data' - }, { - statusCode: 200, - body: '["hippo", "giraffe"]' - }); - - loadFlowCode([ - { "type": "init" }, - { - "type": "http", - "method": "GET", - "endpoint": "https://example.com/data" - }, - { - "type": "debug", - "open": 2, - "showData": true - } - ]); - cy.contains('hippo'); - cy.contains('giraffe'); - }); - it('should return first results only if not paginated, with proxy', () => { cy.intercept({ url: 'https://proxy.kendra.io/', @@ -191,6 +260,4 @@ describe('HTTP Block Integration Tests', () => { // we check it does not contain a second page result: cy.get('body').should('not.contain', 'fish'); }); - - }); \ No newline at end of file diff --git a/docs/workflow/blocks/http.rst b/docs/workflow/blocks/http.rst index 81cf19068..38d454a2a 100644 --- a/docs/workflow/blocks/http.rst +++ b/docs/workflow/blocks/http.rst @@ -33,12 +33,13 @@ Supported properties property. - **headers** - A set of headers with header name as object key. Values are processed by JMESpath - **endpoint** - The request endpoint. Can take multiple forms. See below. +- **onError** - Define an array of blocks to show when there is an error processing the HTTP request. Examples -------- -For simple requests, the ``endpoint`` can just be a simple string: +**Default** For simple requests, the ``endpoint`` can just be a simple string: .. code-block:: json @@ -50,7 +51,8 @@ For simple requests, the ``endpoint`` can just be a simple string: } -If the endpoint needs to be constructed from data, the endpoint can be specified as an object with a "valueGetter" attribute. +**Dynamic data** If the endpoint needs to be constructed from data, the endpoint can be specified as an object with a "valueGetter" attribute. +"valueGetter" can only get data from the context. .. code-block:: json @@ -62,9 +64,26 @@ If the endpoint needs to be constructed from data, the endpoint can be specified } } +.. code-block:: json + + { + "type": "http", + "method": "get", + "endpoint": { + "protocol": "https:", + "host": "api.harvestapp.com/api/v2", + "pathname": "/reports/time/tasks", + "valueGetters": { + "query": "{ from: context.savedData.from, to: context.savedData.to }" + } + } + } + +**Headers** For advanced use cases, the payload can be constructed using a JMES Path expression. -Custom headers can also be specified using JMES Path expressions: +JMESPath expressions can be used to dynamically set header and payload values. +Caution: if the header value is a string, it must use two types of quotes: double and single quotes, like "payload": "'grant_type=client_credentials'". .. code-block:: json @@ -83,7 +102,7 @@ Custom headers can also be specified using JMES Path expressions: } } -It is possible to query a GraphQL endpoint using the HTTP block. +**GraphQL** It is possible to query a GraphQL endpoint using the HTTP block. .. code-block:: json @@ -100,6 +119,41 @@ It is possible to query a GraphQL endpoint using the HTTP block. } +**onError** To debug and display an error message + +.. code-block:: json + + { + "type": "http", + "method": "get", + "endpoint": { + "protocol": "https:", + "host": "accounts.spotify.com", + "pathname": "/api/token" + }, + "onError": { + "blocks": [ + { + "type": "debug", + "open": 1, + "showData": true, + "showContext": false, + "showState": false + }, + { + "type": "card", + "blocks": [ + { + "type": "template", + "template": "Error with submission:

{{data.error.error}} - {{data.error.error_description}}

" + } + ] + } + ] + } + } + + Pagination ---------- diff --git a/src/app/blocks/http-block/http-block.component.ts b/src/app/blocks/http-block/http-block.component.ts index 640f02bb0..6dab70cc5 100644 --- a/src/app/blocks/http-block/http-block.component.ts +++ b/src/app/blocks/http-block/http-block.component.ts @@ -6,7 +6,6 @@ import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack import { catchError, expand, reduce, takeWhile } from 'rxjs/operators'; import { of, EMPTY } from 'rxjs'; import { mappingUtility } from '../mapping-block/mapping-util'; - @Component({ selector: 'app-http-block', templateUrl: './http-block.component.html', @@ -63,6 +62,7 @@ export class HttpBlockComponent implements OnInit, OnChanges { } this.responseType = get(this.config, 'responseType', 'json'); this.errorBlocks = get(this.config, 'onError.blocks', []); + this.makeRequest(); } @@ -144,12 +144,14 @@ export class HttpBlockComponent implements OnInit, OnChanges { this.errorMessage = error.message; this.errorData = error; // TODO: need to prevent errors for triggering subsequent blocks - return of([]); + return of({error, hasError: this.hasError, errorMessage: this.errorMessage}); }) ) - .subscribe(response => { + .subscribe((response: Record) => { this.isLoading = false; this.hasError = false; + if(!response.hasError) this.errorBlocks = []; + this.outputResult(response); }); } @@ -162,12 +164,14 @@ export class HttpBlockComponent implements OnInit, OnChanges { this.errorMessage = error.message; this.errorData = error; // TODO: need to prevent errors for triggering subsequent blocks - return of([]); + return of({error, hasError: this.hasError, errorMessage: this.errorMessage}); }) ) - .subscribe(response => { + .subscribe((response: Record) => { this.isLoading = false; this.hasError = false; + if(!response.hasError) this.errorBlocks = []; + this.outputResult(response); }); break; @@ -190,12 +194,14 @@ export class HttpBlockComponent implements OnInit, OnChanges { this.errorMessage = error.message; this.errorData = error; // TODO: need to prevent errors for triggering subsequent blocks - return of([]); + return of({error, hasError: this.hasError, errorMessage: this.errorMessage}); }) ) - .subscribe(response => { + .subscribe((response: Record) => { this.isLoading = false; this.hasError = false; + if(!response.hasError) this.errorBlocks = []; + this.outputResult(response); const notify = get(this.config, 'notify', true); if (notify) { @@ -237,12 +243,14 @@ export class HttpBlockComponent implements OnInit, OnChanges { this.errorMessage = error.message; this.errorData = error; // TODO: need to prevent errors for triggering subsequent blocks - return of([]); + return of({error, hasError: this.hasError, errorMessage: this.errorMessage}); }) ) - .subscribe(response => { + .subscribe((response: Record) => { this.isLoading = false; this.hasError = false; + if(!response.hasError) this.errorBlocks = []; + this.outputResult(response); const notify = get(this.config, 'notify', true); if (notify) { @@ -382,6 +390,7 @@ export class HttpBlockComponent implements OnInit, OnChanges { const pathname = get(endpoint, 'pathname', '/'); const query = get(endpoint, 'query', []); const reduceQuery = _q => Object.keys(_q).map(key => `${key}=${_q[key]}`, []).join('&'); + return `${protocol}//${host}${pathname}?${reduceQuery(query)}`; } }