jmeter-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pmoua...@apache.org
Subject [jmeter] branch master updated: BZ 64752 - Add GraphQL/HTTP Request Sampler (#627)
Date Sun, 04 Oct 2020 09:07:05 GMT
This is an automated email from the ASF dual-hosted git repository.

pmouawad pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git


The following commit(s) were added to refs/heads/master by this push:
     new 82e56be  BZ 64752 - Add GraphQL/HTTP Request Sampler  (#627)
82e56be is described below

commit 82e56beb04505af96d1e9ce327f67aa8eba99e06
Author: Woonsan Ko <woonsan@users.noreply.github.com>
AuthorDate: Sun Oct 4 05:06:56 2020 -0400

    BZ 64752 - Add GraphQL/HTTP Request Sampler  (#627)
    
    * BZ-64752: adding GraphQL HTTP Sampler GUI components
    
    * BZ-64752: javadocs
    
    * BZ-64752: fixing tab selection problem; disable tab validation in graphql ui
    
    * BZ-64752: (de)serializing test element with graphql query and vars
    
    * BZ-64752: (de)serializing using gson
    
    * BZ-64752: removing unnecessary GraphQLHTTPSampler
    
    * BZ-64752: adding operationName input field
    
    * BZ-64752: support GET method
    
    * BZ-64752: init operationName from test elem
    
    * BZ-64752: adding a simple graphql test plan demo
    
    * BZ-64752: show advanced pane
    
    * BZ-64752: add gson info to lib/aareadme.txt
    
    * BZ-64752: screenshot and default constructor
    
    * BZ-64752: documentation on GraphQLHTTPRequest
    
    * BZ-64752: record in changes.xml
    
    * BZ-64752: add gson.jar to expected_release_jars.csv
    
    * BZ-64752: removing unnecessary, untranslated messages
    
    * BZ-64752: utility for graphql param serialization and unit test
    
    * BZ-64752: replace gson with jackson for graphql (de)serialization
    
    * BZ-64752: remove gson jar from expected release jars
    
    * BZ-64752: correcting French translation, thanks to pmouawad
    
    * BZ-64752: graphql http recording support
    
    * BZ-64752: checkbox option to switch on/off auto graphql req detection, true by default
    
    * BZ-64752: precise json content type checking; encode in GET
    
    * BZ-64752: French translation for graphql recording option, thanks to @ubikloadpack
    
    Co-authored-by: Woonsan Ko <woonsan.ko@bloomreach.com>
---
 bin/saveservice.properties                         |   1 +
 .../java/org/apache/jmeter/save/SaveService.java   |   2 +-
 .../org/apache/jmeter/gui/util/textarea.properties |   1 +
 .../apache/jmeter/resources/messages.properties    |   7 +
 .../apache/jmeter/resources/messages_fr.properties |   7 +
 src/protocol/build.gradle.kts                      |   2 +
 .../protocol/http/config/GraphQLRequestParams.java |  67 +++++
 .../config/gui/AbstractValidationTabbedPane.java   |  86 +++++++
 .../http/config/gui/GraphQLUrlConfigGui.java       | 190 ++++++++++++++
 .../http/config/gui/UrlConfigDefaults.java         | 272 +++++++++++++++++++++
 .../protocol/http/config/gui/UrlConfigGui.java     | 129 +++++-----
 .../protocol/http/control/HeaderManager.java       |  20 ++
 .../http/control/gui/GraphQLHTTPSamplerGui.java    |  75 ++++++
 .../http/control/gui/HttpTestSampleGui.java        |  62 +++--
 .../protocol/http/proxy/DefaultSamplerCreator.java |  44 ++++
 .../jmeter/protocol/http/proxy/HttpRequestHdr.java |  18 ++
 .../apache/jmeter/protocol/http/proxy/Proxy.java   |   2 +
 .../jmeter/protocol/http/proxy/ProxyControl.java   |  12 +
 .../protocol/http/proxy/gui/ProxyControlGui.java   |  22 ++
 .../http/util/GraphQLRequestParamUtils.java        | 254 +++++++++++++++++++
 .../sampler/GraphQLHTTPSamplerResources.properties |  27 ++
 .../http/util/TestGraphQLRequestParamUtils.java    | 212 ++++++++++++++++
 xdocs/changes.xml                                  |   1 +
 xdocs/demos/SimpleGraphQLTestPlan.jmx              | 135 ++++++++++
 .../screenshots/graphql-http-request-vars.png      | Bin 0 -> 110286 bytes
 xdocs/images/screenshots/graphql-http-request.png  | Bin 0 -> 115587 bytes
 xdocs/usermanual/component_reference.xml           |  28 ++-
 27 files changed, 1596 insertions(+), 80 deletions(-)

diff --git a/bin/saveservice.properties b/bin/saveservice.properties
index 6f0b44b..b2ec167 100644
--- a/bin/saveservice.properties
+++ b/bin/saveservice.properties
@@ -150,6 +150,7 @@ GaussianRandomTimerGui=org.apache.jmeter.timers.gui.GaussianRandomTimerGui
 GenericController=org.apache.jmeter.control.GenericController
 GraphAccumVisualizer=org.apache.jmeter.visualizers.GraphAccumVisualizer
 GraphVisualizer=org.apache.jmeter.visualizers.GraphVisualizer
+GraphQLHTTPSamplerGui=org.apache.jmeter.protocol.http.control.gui.GraphQLHTTPSamplerGui
 Header=org.apache.jmeter.protocol.http.control.Header
 HeaderManager=org.apache.jmeter.protocol.http.control.HeaderManager
 HeaderPanel=org.apache.jmeter.protocol.http.gui.HeaderPanel
diff --git a/src/core/src/main/java/org/apache/jmeter/save/SaveService.java b/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
index 0d7cf07..2955c38 100644
--- a/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
+++ b/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
@@ -155,7 +155,7 @@ public class SaveService {
     private static String fileVersion = ""; // computed from saveservice.properties file// $NON-NLS-1$
     // Must match the sha1 checksum of the file saveservice.properties (without newline character),
     // used to ensure saveservice.properties and SaveService are updated simultaneously
-    static final String FILEVERSION = "56ae8319b2b02d33eb1028c4460db770cf246b5c"; // Expected value $NON-NLS-1$
+    static final String FILEVERSION = "66ea47f7da884dff1c42ccede75113971c5c11f3"; // Expected value $NON-NLS-1$
 
     private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
 
diff --git a/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties b/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties
index 76b5365..f587f3b 100644
--- a/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties
@@ -37,6 +37,7 @@ jexl3 = text/java
 jpython = text/python
 js = text/javascript
 jscript = text/javascript
+json = text/json
 judoscript = text/plain
 jython = text/python
 lisp = text/lisp
diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
index 9013ed1..903e9db 100644
--- a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
@@ -293,6 +293,7 @@ deltest=Deletion test
 deref=Dereference aliases
 description=Description
 detail=Detail
+detect_graphql_request=Detect GraphQL Request
 directory_field_title=Working directory:
 disable=Disable
 dn=DN
@@ -449,6 +450,11 @@ graph_results_ms=ms
 graph_results_no_samples=No of Samples
 graph_results_throughput=Throughput
 graph_results_title=Graph Results
+graphql_http_sampler_title=GraphQL HTTP Request
+graphql_request_info=GraphQL Request
+graphql_operation_name=Operation Name
+graphql_query=Query
+graphql_variables=Variables
 groovy_function_expression=Expression to evaluate
 grouping_add_separators=Add separators between groups
 grouping_in_controllers=Put each group in a new controller
@@ -864,6 +870,7 @@ proxy_headers=Capture HTTP Headers
 proxy_pause_http_sampler=Create new transaction after request (ms)\:
 proxy_recorder_dialog=Recorder\: Transactions Control
 proxy_regex=Regex matching
+proxy_sampler_graphql_settings=GraphQL HTTP Sampler settings
 proxy_sampler_settings=HTTP Sampler settings
 proxy_sampler_type=Type\:
 proxy_separators=Add Separators
diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties b/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
index 0dc5426..ddb10a2 100644
--- a/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
@@ -288,6 +288,7 @@ deltest=Suppression
 deref=Déréférencement des alias
 description=Description
 detail=Détail
+detect_graphql_request=Détecter les requêtes GraphQL
 directory_field_title=Répertoire d'exécution \:
 disable=Désactiver
 dn=Racine DN \:
@@ -443,6 +444,11 @@ graph_results_ms=ms
 graph_results_no_samples=Nombre d'échantillons
 graph_results_throughput=Débit
 graph_results_title=Graphique de résultats
+graphql_http_sampler_title=Requête HTTP GraphQL
+graphql_request_info=Requête GraphQL
+graphql_operation_name=Nom de l'opération
+graphql_query=Requête
+graphql_variables=Variables
 groovy_function_expression=Expression à évaluer
 grouping_add_separators=Ajouter des séparateurs entre les groupes
 grouping_in_controllers=Mettre chaque groupe dans un nouveau contrôleur
@@ -853,6 +859,7 @@ proxy_headers=Capturer les entêtes HTTP
 proxy_pause_http_sampler=Créer une nouvelle transaction après la requête (ms) \:
 proxy_recorder_dialog=Enregistreur\: Contrôle des transactions
 proxy_regex=Correspondance des variables par regex ?
+proxy_sampler_graphql_settings=Configuration de la requête GraphQL
 proxy_sampler_settings=Paramètres Echantillon HTTP
 proxy_sampler_type=Type \:
 proxy_separators=Ajouter des séparateurs
diff --git a/src/protocol/build.gradle.kts b/src/protocol/build.gradle.kts
index aca2843..d0855cf 100644
--- a/src/protocol/build.gradle.kts
+++ b/src/protocol/build.gradle.kts
@@ -85,6 +85,8 @@ project("http") {
         implementation("org.apache.httpcomponents:httpcore")
         implementation("org.brotli:dec")
         implementation("com.miglayout:miglayout-swing")
+        implementation("com.fasterxml.jackson.core:jackson-core")
+        implementation("com.fasterxml.jackson.core:jackson-databind")
         testImplementation(testFixtures(project(":src:testkit-wiremock")))
         testImplementation("com.github.tomakehurst:wiremock-jre8")
     }
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/GraphQLRequestParams.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/GraphQLRequestParams.java
new file mode 100644
index 0000000..6edabbf
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/GraphQLRequestParams.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.config;
+
+import java.io.Serializable;
+
+/**
+ * Represents GraphQL request parameter input data for Query, Variables and Operation Name.
+ */
+public class GraphQLRequestParams implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String operationName;
+
+    private String query;
+
+    private String variables;
+
+    public GraphQLRequestParams() {
+    }
+
+    public GraphQLRequestParams(final String operationName, final String query, final String variables) {
+        this.operationName = operationName;
+        this.query = query;
+        this.variables = variables;
+    }
+
+    public String getOperationName() {
+        return operationName;
+    }
+
+    public void setOperationName(String operationName) {
+        this.operationName = operationName;
+    }
+
+    public String getQuery() {
+        return query;
+    }
+
+    public void setQuery(String query) {
+        this.query = query;
+    }
+
+    public String getVariables() {
+        return variables;
+    }
+
+    public void setVariables(String variables) {
+        this.variables = variables;
+    }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/AbstractValidationTabbedPane.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/AbstractValidationTabbedPane.java
new file mode 100644
index 0000000..bc8c942
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/AbstractValidationTabbedPane.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.config.gui;
+
+import javax.swing.JTabbedPane;
+
+/**
+ * Abstract {@link JTabbedPane} to allow validating the requested tab index, updating states and changing the tab index
+ * after the validation if necessary.
+ */
+abstract class AbstractValidationTabbedPane extends JTabbedPane {
+
+    private static final long serialVersionUID = 7014311238367882880L;
+
+    /**
+     * Flag whether the validation feature should be enabled or not, {@code true} by default.
+     */
+    private boolean validationEnabled = true;
+
+    /**
+     * {@inheritDoc}
+     * <P>
+     * Overridden to delegate to {@link #setSelectedIndex(int, boolean)} in order to validate the requested tab index by default.
+     */
+    @Override
+    public void setSelectedIndex(int index) {
+        setSelectedIndex(index, true);
+    }
+
+    /**
+     * Apply some check rules by invoking {@link #getValidatedTabIndex(int, int)}
+     * if {@link #isValidationEnabled()} returns true and the {@code check} input is true.
+     *
+     * @param index index to select
+     * @param check flag whether to perform checks before setting the selected index
+     */
+    public void setSelectedIndex(int index, boolean check) {
+        final int curIndex = super.getSelectedIndex();
+
+        if (!isValidationEnabled() || !check || curIndex == -1) {
+            super.setSelectedIndex(index);
+            return;
+        }
+
+        super.setSelectedIndex(getValidatedTabIndex(curIndex, index));
+    }
+
+    /**
+     * Validate the requested tab index ({@code newTabIndex}) and return a validated tab index after applying some check rules.
+     * @param currentTabIndex current tab index
+     * @param newTabIndex new requested tab index to validate
+     * @return the validated tab index
+     */
+    abstract protected int getValidatedTabIndex(final int currentTabIndex, final int newTabIndex);
+
+    /**
+     * Return true if the validation feature should be enabled, {@code true} by default.
+     * @return true if the validation feature should be enabled, {@code true} by default
+     */
+    protected boolean isValidationEnabled() {
+        return validationEnabled;
+    }
+
+    /**
+     * Set the flag whether the validation feature should be enabled or not.
+     * @param validationEnabled flag whether the validation feature should be enabled or not
+     */
+    protected void setValidationEnabled(boolean validationEnabled) {
+        this.validationEnabled = validationEnabled;
+    }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/GraphQLUrlConfigGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/GraphQLUrlConfigGui.java
new file mode 100644
index 0000000..38b3824
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/GraphQLUrlConfigGui.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.config.gui;
+
+import java.awt.Component;
+
+import javax.swing.BorderFactory;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.gui.util.HorizontalPanel;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
+import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
+import org.apache.jmeter.protocol.http.util.GraphQLRequestParamUtils;
+import org.apache.jmeter.protocol.http.util.HTTPArgument;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.testelement.TestElement;
+import org.apache.jmeter.testelement.property.TestElementProperty;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jorphan.gui.JLabeledTextField;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Extending {@link UrlConfigGui}, GraphQL over HTTP Request configuration GUI, providing more convenient UI elements
+ * for GraphQL query, variables and operationName.
+ */
+public class GraphQLUrlConfigGui extends UrlConfigGui {
+
+    private static final long serialVersionUID = 1L;
+
+    private static Logger log = LoggerFactory.getLogger(GraphQLUrlConfigGui.class);
+
+    public static final String OPERATION_NAME = "GraphQLHTTPSampler.operationName";
+
+    public static final String QUERY = "GraphQLHTTPSampler.query";
+
+    public static final String VARIABLES = "GraphQLHTTPSampler.variables";
+
+    /**
+     * Default value settings for GraphQL URL Configuration GUI elements.
+     */
+    private static final UrlConfigDefaults URL_CONFIG_DEFAULTS = new UrlConfigDefaults();
+    static {
+        URL_CONFIG_DEFAULTS.setValidMethods(new String[] { HTTPConstants.POST, HTTPConstants.GET });
+        URL_CONFIG_DEFAULTS.setDefaultMethod(HTTPConstants.POST);
+        URL_CONFIG_DEFAULTS.setAutoRedirects(false);
+        URL_CONFIG_DEFAULTS.setFollowRedirects(false);
+        URL_CONFIG_DEFAULTS.setUseBrowserCompatibleMultipartMode(false);
+        URL_CONFIG_DEFAULTS.setUseKeepAlive(true);
+        URL_CONFIG_DEFAULTS.setUseMultipart(false);
+        URL_CONFIG_DEFAULTS.setUseMultipartVisible(false);
+    }
+
+    private JLabeledTextField operationNameText;
+
+    private JSyntaxTextArea queryContent;
+
+    private JSyntaxTextArea variablesContent;
+
+    /**
+     * Constructor which is setup to show the sampler fields for GraphQL over HTTP request.
+     */
+    public GraphQLUrlConfigGui() {
+        super(true, false, false);
+    }
+
+    @Override
+    public void configure(TestElement element) {
+        super.configure(element);
+        final String operationName = element.getPropertyAsString(OPERATION_NAME, "");
+        operationNameText.setText(operationName);
+        final String query = element.getPropertyAsString(QUERY, "");
+        queryContent.setText(query);
+        final String variables = element.getPropertyAsString(VARIABLES, "");
+        variablesContent.setText(variables);
+    }
+
+    @Override
+    public void modifyTestElement(TestElement element) {
+        super.modifyTestElement(element);
+
+        final String method = element.getPropertyAsString(HTTPSamplerBase.METHOD);
+        final GraphQLRequestParams params = new GraphQLRequestParams(operationNameText.getText(),
+                queryContent.getText(), variablesContent.getText());
+
+        element.setProperty(OPERATION_NAME, params.getOperationName());
+        element.setProperty(QUERY, params.getQuery());
+        element.setProperty(VARIABLES, params.getVariables());
+        element.setProperty(HTTPSamplerBase.POST_BODY_RAW, !HTTPConstants.GET.equals(method));
+
+        final Arguments args;
+
+        if (HTTPConstants.GET.equals(method)) {
+            args = createHTTPArgumentsTestElement();
+
+            if (StringUtils.isNotBlank(params.getOperationName())) {
+                args.addArgument(createHTTPArgument("operationName", params.getOperationName().trim(), true));
+            }
+
+            args.addArgument(createHTTPArgument("query",
+                    GraphQLRequestParamUtils.queryToGetParamValue(params.getQuery()), true));
+
+            if (StringUtils.isNotBlank(params.getVariables())) {
+                final String variablesParamValue = GraphQLRequestParamUtils
+                        .variablesToGetParamValue(params.getVariables());
+                if (variablesParamValue != null) {
+                    args.addArgument(createHTTPArgument("variables", variablesParamValue, true));
+                }
+            }
+        } else {
+            args = new Arguments();
+            args.addArgument(createHTTPArgument("", GraphQLRequestParamUtils.toPostBodyString(params), false));
+        }
+
+        element.setProperty(new TestElementProperty(HTTPSamplerBase.ARGUMENTS, args));
+    }
+
+    @Override
+    protected UrlConfigDefaults getUrlConfigDefaults() {
+        return URL_CONFIG_DEFAULTS;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <P>
+     * Overridden to add the extra GraphQL Request Information section including 'operationName' text field.
+     */
+    @Override
+    protected Component getPathPanel() {
+        final JPanel panel = (JPanel) super.getPathPanel();
+        JPanel graphQLReqInfoPane = new HorizontalPanel();
+        graphQLReqInfoPane
+                .setBorder(BorderFactory.createTitledBorder(JMeterUtils.getResString("graphql_request_info")));
+        operationNameText = new JLabeledTextField(JMeterUtils.getResString("graphql_operation_name"), 40);
+        graphQLReqInfoPane.add(operationNameText);
+        panel.add(graphQLReqInfoPane);
+        return panel;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <P>
+     * Overridden to remove the existing tab for parameter arguments and GraphQL variables content pane.
+     */
+    @Override
+    protected JTabbedPane getParameterPanel() {
+        final AbstractValidationTabbedPane paramPanel = (AbstractValidationTabbedPane) super.getParameterPanel();
+        paramPanel.removeAll();
+        paramPanel.setValidationEnabled(false);
+
+        queryContent = JSyntaxTextArea.getInstance(26, 50);
+        queryContent.setInitialText("");
+        paramPanel.add(JMeterUtils.getResString("graphql_query"), JTextScrollPane.getInstance(queryContent));
+
+        variablesContent = JSyntaxTextArea.getInstance(26, 50);
+        variablesContent.setLanguage("json");
+        variablesContent.setInitialText("");
+        paramPanel.add(JMeterUtils.getResString("graphql_variables"), JTextScrollPane.getInstance(variablesContent));
+
+        return paramPanel;
+    }
+
+    private HTTPArgument createHTTPArgument(final String name, final String value, final boolean alwaysEncoded) {
+        final HTTPArgument arg = new HTTPArgument(name, value);
+        arg.setUseEquals(true);
+        arg.setEnabled(true);
+        arg.setAlwaysEncoded(alwaysEncoded);
+        return arg;
+    }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigDefaults.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigDefaults.java
new file mode 100644
index 0000000..e830628
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigDefaults.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.config.gui;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
+
+/**
+ * Default option value settings for {@link UrlConfigGui}.
+ */
+public class UrlConfigDefaults implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Available HTTP methods to be shown in the {@link UrlConfigGui}.
+     */
+    private List<String> validMethodList;
+
+    /**
+     * The default HTTP method to be selected in the {@link UrlConfigGui}.
+     */
+    private String defaultMethod = HTTPSamplerBase.DEFAULT_METHOD;
+
+    /**
+     * The default value to be set for the followRedirect checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean followRedirects = true;
+
+    /**
+     * The default value to be set for the autoRedirects checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean autoRedirects;
+
+    /**
+     * The default value to be set for the useKeepAlive checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean useKeepAlive = true;
+
+    /**
+     * The default value to be set for the useMultipart checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean useMultipart;
+
+    /**
+     * The default value to be set for the useBrowserCompatibleMultipartMode checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean useBrowserCompatibleMultipartMode = HTTPSamplerBase.BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT;
+
+    /**
+     * Flag whether to show the followRedirect checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean followRedirectsVisible = true;
+
+    /**
+     * Flag whether to show the autoRedirectsVisible checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean autoRedirectsVisible = true;
+
+    /**
+     * Flag whether to show the useKeepAliveVisible checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean useKeepAliveVisible = true;
+
+    /**
+     * Flag whether to show the useMultipartVisible checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean useMultipartVisible = true;
+
+    /**
+     * Flag whether to show the useBrowserCompatibleMultipartModeVisible checkbox in the {@link UrlConfigGui}.
+     */
+    private boolean useBrowserCompatibleMultipartModeVisible = true;
+
+    /**
+     * Return available HTTP methods to be shown in the {@link UrlConfigGui}, returning {@link HTTPSamplerBase#getValidMethodsAsArray()}
+     * by default if not reset.
+     * @return available HTTP methods to be shown in the {@link UrlConfigGui}
+     */
+    public String[] getValidMethods() {
+        if (validMethodList != null) {
+            return validMethodList.toArray(new String[validMethodList.size()]);
+        }
+        return HTTPSamplerBase.getValidMethodsAsArray();
+    }
+
+    /**
+     * Set available HTTP methods to be shown in the {@link UrlConfigGui}.
+     * @param validMethods available HTTP methods
+     * @throws IllegalArgumentException if the input array is empty
+     */
+    public void setValidMethods(String[] validMethods) {
+        if (validMethods == null || validMethods.length == 0) {
+            throw new IllegalArgumentException("HTTP methods array is empty.");
+        }
+        this.validMethodList = Arrays.asList(validMethods);
+    }
+
+    /**
+     * Return the default HTTP method to be selected in the {@link UrlConfigGui}.
+     * @return the default HTTP method to be selected in the {@link UrlConfigGui}
+     */
+    public String getDefaultMethod() {
+        return defaultMethod;
+    }
+
+    /**
+     * Set the default HTTP method to be selected in the {@link UrlConfigGui}.
+     * @param defaultMethod the default HTTP method to be selected in the {@link UrlConfigGui}
+     */
+    public void setDefaultMethod(String defaultMethod) {
+        this.defaultMethod = defaultMethod;
+    }
+
+    /**
+     * Return the default value to be set for the followRedirect checkbox in the {@link UrlConfigGui}.
+     */
+    public boolean isFollowRedirects() {
+        return followRedirects;
+    }
+
+    /**
+     * Set the default value to be set for the followRedirect checkbox in the {@link UrlConfigGui}.
+     */
+    public void setFollowRedirects(boolean followRedirects) {
+        this.followRedirects = followRedirects;
+    }
+
+    /**
+     * Return the default value to be set for the autoRedirects checkbox in the {@link UrlConfigGui}.
+     */
+    public boolean isAutoRedirects() {
+        return autoRedirects;
+    }
+
+    /**
+     * Set the default value to be set for the autoRedirects checkbox in the {@link UrlConfigGui}.
+     */
+    public void setAutoRedirects(boolean autoRedirects) {
+        this.autoRedirects = autoRedirects;
+    }
+
+    /**
+     * Return the default value to be set for the useKeepAlive checkbox in the {@link UrlConfigGui}.
+     */
+    public boolean isUseKeepAlive() {
+        return useKeepAlive;
+    }
+
+    /**
+     * Set the default value to be set for the useKeepAlive checkbox in the {@link UrlConfigGui}.
+     */
+    public void setUseKeepAlive(boolean useKeepAlive) {
+        this.useKeepAlive = useKeepAlive;
+    }
+
+    /**
+     * Return the default value to be set for the useMultipart checkbox in the {@link UrlConfigGui}.
+     */
+    public boolean isUseMultipart() {
+        return useMultipart;
+    }
+
+    /**
+     * Set the default value to be set for the useMultipart checkbox in the {@link UrlConfigGui}.
+     */
+    public void setUseMultipart(boolean useMultipart) {
+        this.useMultipart = useMultipart;
+    }
+
+    /**
+     * Return the default value to be set for the useBrowserCompatibleMultipartMode checkbox in the {@link UrlConfigGui}.
+     */
+    public boolean isUseBrowserCompatibleMultipartMode() {
+        return useBrowserCompatibleMultipartMode;
+    }
+
+    /**
+     * Set the default value to be set for the useBrowserCompatibleMultipartMode checkbox in the {@link UrlConfigGui}.
+     */
+    public void setUseBrowserCompatibleMultipartMode(boolean useBrowserCompatibleMultipartMode) {
+        this.useBrowserCompatibleMultipartMode = useBrowserCompatibleMultipartMode;
+    }
+
+    /**
+     * Return true if the followRedirect checkbox should be visible in the {@link UrlConfigGui}.
+     */
+    public boolean isFollowRedirectsVisible() {
+        return followRedirectsVisible;
+    }
+
+    /**
+     * Set the visibility of the followRedirect checkbox in the {@link UrlConfigGui}.
+     */
+    public void setFollowRedirectsVisible(boolean followRedirectsVisible) {
+        this.followRedirectsVisible = followRedirectsVisible;
+    }
+
+    /**
+     * Return true if the autoRedirectsVisible checkbox should be visible in the {@link UrlConfigGui}.
+     */
+    public boolean isAutoRedirectsVisible() {
+        return autoRedirectsVisible;
+    }
+
+    /**
+     * Set the visibility of the autoRedirectsVisible checkbox in the {@link UrlConfigGui}.
+     */
+    public void setAutoRedirectsVisible(boolean autoRedirectsVisible) {
+        this.autoRedirectsVisible = autoRedirectsVisible;
+    }
+
+    /**
+     * Return true if the useKeepAliveVisible checkbox should be visible in the {@link UrlConfigGui}.
+     */
+    public boolean isUseKeepAliveVisible() {
+        return useKeepAliveVisible;
+    }
+
+    /**
+     * Set the visibility of the useKeepAliveVisible checkbox in the {@link UrlConfigGui}.
+     */
+    public void setUseKeepAliveVisible(boolean useKeepAliveVisible) {
+        this.useKeepAliveVisible = useKeepAliveVisible;
+    }
+
+    /**
+     * Return true if the useMultipartVisible checkbox should by default in the {@link UrlConfigGui}.
+     */
+    public boolean isUseMultipartVisible() {
+        return useMultipartVisible;
+    }
+
+    /**
+     * Set the visibility of the useMultipartVisible checkbox in the {@link UrlConfigGui}.
+     */
+    public void setUseMultipartVisible(boolean useMultipartVisible) {
+        this.useMultipartVisible = useMultipartVisible;
+    }
+
+    /**
+     * Return true if the useBrowserCompatibleMultipartModeVisible checkbox should be visible in the {@link UrlConfigGui}.
+     */
+    public boolean isUseBrowserCompatibleMultipartModeVisible() {
+        return useBrowserCompatibleMultipartModeVisible;
+    }
+
+    /**
+     * Set the visibility of the useBrowserCompatibleMultipartModeVisible checkbox in the {@link UrlConfigGui}.
+     */
+    public void setUseBrowserCompatibleMultipartModeVisible(boolean useBrowserCompatibleMultipartModeVisible) {
+        this.useBrowserCompatibleMultipartModeVisible = useBrowserCompatibleMultipartModeVisible;
+    }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
index 2fd4a69..4f29b4b 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
@@ -63,6 +63,11 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
 
     private static final long serialVersionUID = 240L;
 
+    /**
+     * Default value settings for URL Configuration GUI elements.
+     */
+    private static final UrlConfigDefaults URL_CONFIG_DEFAULTS = new UrlConfigDefaults();
+
     private static final int TAB_PARAMETERS = 0;
 
     private int tabRawBodyIndex = 1;
@@ -106,7 +111,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
     private JSyntaxTextArea postBodyContent;
 
     // Tabbed pane that contains parameters and raw body
-    private ValidationTabbedPane postContentTabbedPane;
+    private AbstractValidationTabbedPane postContentTabbedPane;
 
     private boolean showRawBodyPane;
     private boolean showFileUploadPane;
@@ -156,12 +161,12 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
     public void clear() {
         domain.setText(""); // $NON-NLS-1$
         if (notConfigOnly){
-            followRedirects.setSelected(true);
-            autoRedirects.setSelected(false);
-            method.setText(HTTPSamplerBase.DEFAULT_METHOD);
-            useKeepAlive.setSelected(true);
-            useMultipart.setSelected(false);
-            useBrowserCompatibleMultipartMode.setSelected(HTTPSamplerBase.BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT);
+            followRedirects.setSelected(getUrlConfigDefaults().isFollowRedirects());
+            autoRedirects.setSelected(getUrlConfigDefaults().isAutoRedirects());
+            method.setText(getUrlConfigDefaults().getDefaultMethod());
+            useKeepAlive.setSelected(getUrlConfigDefaults().isUseKeepAlive());
+            useMultipart.setSelected(getUrlConfigDefaults().isUseMultipart());
+            useBrowserCompatibleMultipartMode.setSelected(getUrlConfigDefaults().isUseBrowserCompatibleMultipartMode());
         }
         path.setText(""); // $NON-NLS-1$
         port.setText(""); // $NON-NLS-1$
@@ -193,7 +198,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
      * @param element {@link TestElement} to modify
      */
     public void modifyTestElement(TestElement element) {
-        boolean useRaw = !postBodyContent.getText().isEmpty();
+        boolean useRaw = showRawBodyPane && !postBodyContent.getText().isEmpty();
         Arguments args;
         if(useRaw) {
             args = new Arguments();
@@ -273,18 +278,21 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
         setName(el.getName());
         Arguments arguments = (Arguments) el.getProperty(HTTPSamplerBase.ARGUMENTS).getObjectValue();
 
-        boolean useRaw = el.getPropertyAsBoolean(HTTPSamplerBase.POST_BODY_RAW, HTTPSamplerBase.POST_BODY_RAW_DEFAULT);
-        if(useRaw) {
-            String postBody = computePostBody(arguments, true); // Convert CRLF to CR, see modifyTestElement
-            postBodyContent.setInitialText(postBody);
-            postBodyContent.setCaretPosition(0);
-            argsPanel.clear();
-            postContentTabbedPane.setSelectedIndex(tabRawBodyIndex, false);
-        } else {
-            postBodyContent.setInitialText("");
-            argsPanel.configure(arguments);
-            postContentTabbedPane.setSelectedIndex(TAB_PARAMETERS, false);
+        if (showRawBodyPane) {
+            boolean useRaw = el.getPropertyAsBoolean(HTTPSamplerBase.POST_BODY_RAW, HTTPSamplerBase.POST_BODY_RAW_DEFAULT);
+            if(useRaw) {
+                String postBody = computePostBody(arguments, true); // Convert CRLF to CR, see modifyTestElement
+                postBodyContent.setInitialText(postBody);
+                postBodyContent.setCaretPosition(0);
+                argsPanel.clear();
+                postContentTabbedPane.setSelectedIndex(tabRawBodyIndex, false);
+            } else {
+                postBodyContent.setInitialText("");
+                argsPanel.configure(arguments);
+                postContentTabbedPane.setSelectedIndex(TAB_PARAMETERS, false);
+            }
         }
+
         if(showFileUploadPane) {
             filesPanel.configure(el);
         }
@@ -349,6 +357,13 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
         return webServerPanel;
     }
 
+    /**
+     * Return the {@link UrlConfigDefaults} instance to be used when configuring the UI elements and default values.
+     * @return the {@link UrlConfigDefaults} instance to be used when configuring the UI elements and default values
+     */
+    protected UrlConfigDefaults getUrlConfigDefaults() {
+        return URL_CONFIG_DEFAULTS;
+    }
 
     /**
      * This method defines the Panel for:
@@ -365,33 +380,37 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
 
         if (notConfigOnly){
             method = new JLabeledChoice(JMeterUtils.getResString("method"), // $NON-NLS-1$
-                    HTTPSamplerBase.getValidMethodsAsArray(), true, false);
+                    getUrlConfigDefaults().getValidMethods(), true, false);
             method.addChangeListener(this);
         }
 
         if (notConfigOnly){
             followRedirects = new JCheckBox(JMeterUtils.getResString("follow_redirects")); // $NON-NLS-1$
             JFactory.small(followRedirects);
-            followRedirects.setSelected(true);
+            followRedirects.setSelected(getUrlConfigDefaults().isFollowRedirects());
             followRedirects.addChangeListener(this);
+            followRedirects.setVisible(getUrlConfigDefaults().isFollowRedirectsVisible());
 
             autoRedirects = new JCheckBox(JMeterUtils.getResString("follow_redirects_auto")); //$NON-NLS-1$
             JFactory.small(autoRedirects);
             autoRedirects.addChangeListener(this);
-            autoRedirects.setSelected(false);// Default changed in 2.3 and again in 2.4
+            autoRedirects.setSelected(getUrlConfigDefaults().isAutoRedirects());// Default changed in 2.3 and again in 2.4
+            autoRedirects.setVisible(getUrlConfigDefaults().isAutoRedirectsVisible());
 
             useKeepAlive = new JCheckBox(JMeterUtils.getResString("use_keepalive")); // $NON-NLS-1$
             JFactory.small(useKeepAlive);
-            useKeepAlive.setSelected(true);
+            useKeepAlive.setSelected(getUrlConfigDefaults().isUseKeepAlive());
+            useKeepAlive.setVisible(getUrlConfigDefaults().isUseKeepAliveVisible());
 
             useMultipart = new JCheckBox(JMeterUtils.getResString("use_multipart_for_http_post")); // $NON-NLS-1$
             JFactory.small(useMultipart);
-            useMultipart.setSelected(false);
+            useMultipart.setSelected(getUrlConfigDefaults().isUseMultipart());
+            useMultipart.setVisible(getUrlConfigDefaults().isUseMultipartVisible());
 
             useBrowserCompatibleMultipartMode = new JCheckBox(JMeterUtils.getResString("use_multipart_mode_browser")); // $NON-NLS-1$
             JFactory.small(useBrowserCompatibleMultipartMode);
-            useBrowserCompatibleMultipartMode.setSelected(HTTPSamplerBase.BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT);
-
+            useBrowserCompatibleMultipartMode.setSelected(getUrlConfigDefaults().isUseBrowserCompatibleMultipartMode());
+            useBrowserCompatibleMultipartMode.setVisible(getUrlConfigDefaults().isUseBrowserCompatibleMultipartModeVisible());
         }
 
         JPanel pathPanel =  new HorizontalPanel();
@@ -438,59 +457,47 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
     }
 
     /**
-     *
+     * Create a new {@link Arguments} instance associated with the specific GUI used in this component.
+     * @return a new {@link Arguments} instance associated with the specific GUI used in this component
      */
-    class ValidationTabbedPane extends JTabbedPane {
+    protected Arguments createHTTPArgumentsTestElement() {
+        return (Arguments) argsPanel.createTestElement();
+    }
 
-        /**
-         *
-         */
-        private static final long serialVersionUID = 7014311238367882880L;
+    class ValidationTabbedPane extends AbstractValidationTabbedPane {
 
+        private static final long serialVersionUID = 7014311238367882881L;
 
         @Override
-        public void setSelectedIndex(int index) {
-            setSelectedIndex(index, true);
-        }
-
-        /**
-         * Apply some check rules if check is true
-         *
-         * @param index
-         *            index to select
-         * @param check
-         *            flag whether to perform checks before setting the selected
-         *            index
-         */
-        public void setSelectedIndex(int index, boolean check) {
-            int oldSelectedIndex = this.getSelectedIndex();
-            if(!check || oldSelectedIndex == -1) {
-                super.setSelectedIndex(index);
-            } else if(index == tabFileUploadIndex) { // We're going to File, no problem
-                super.setSelectedIndex(index);
+        protected int getValidatedTabIndex(int currentTabIndex, int newTabIndex) {
+            if (newTabIndex == tabFileUploadIndex) { // We're going to File, no problem
+                return newTabIndex;
             }
+
             // We're moving to Raw or Parameters
-            else if(index != oldSelectedIndex) {
+            if (newTabIndex != currentTabIndex) {
                 // If the Parameter data can be converted (i.e. no names)
                 // we switch
-                if(index == tabRawBodyIndex) {
-                    if(canSwitchToRawBodyPane()) {
+                if (newTabIndex == tabRawBodyIndex) {
+                    if (canSwitchToRawBodyPane()) {
                         convertParametersToRaw();
-                        super.setSelectedIndex(index);
+                        return newTabIndex;
                     } else {
-                        super.setSelectedIndex(TAB_PARAMETERS);
+                        return TAB_PARAMETERS;
                     }
                 }
                 else {
                     // If the Parameter data cannot be converted to Raw, then the user should be
                     // prevented from doing so raise an error dialog
-                    if(canSwitchToParametersTab()) {
-                        super.setSelectedIndex(index);
+                    if (canSwitchToParametersTab()) {
+                        return newTabIndex;
                     } else {
-                        super.setSelectedIndex(tabRawBodyIndex);
+                        return tabRawBodyIndex;
                     }
                 }
             }
+
+            return newTabIndex;
         }
 
         /**
@@ -510,7 +517,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
          * @return true if postBodyContent is empty
          */
         private boolean canSwitchToParametersTab() {
-            return postBodyContent.getText().isEmpty();
+            return showRawBodyPane && postBodyContent.getText().isEmpty();
         }
     }
 
@@ -531,7 +538,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
      * Convert Parameters to Raw Body
      */
     void convertParametersToRaw() {
-        if(postBodyContent.getText().isEmpty()) {
+        if (showRawBodyPane && postBodyContent.getText().isEmpty()) {
             postBodyContent.setInitialText(computePostBody((Arguments)argsPanel.createTestElement()));
             postBodyContent.setCaretPosition(0);
         }
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java
index aaf675a..02d5b39 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java
@@ -200,6 +200,26 @@ public class HeaderManager extends ConfigTestElement implements Serializable, Re
     }
 
     /**
+     * Get the first header from Headers by the header name, or {@code null} if not found.
+     * @param name header name
+     * @return the first header from Headers by the header name, or {@code null} if not found
+     */
+    public Header getFirstHeaderNamed(final String name) {
+        final CollectionProperty headers = getHeaders();
+        final int size = headers.size();
+        for (int i = 0; i < size; i++) {
+            Header header = (Header) headers.get(i).getObjectValue();
+            if (header == null) {
+                continue;
+            }
+            if (header.getName().equalsIgnoreCase(name)) {
+                return header;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Remove from Headers the header named name
      * @param name header name
      */
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/GraphQLHTTPSamplerGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/GraphQLHTTPSamplerGui.java
new file mode 100644
index 0000000..c393e77
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/GraphQLHTTPSamplerGui.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.control.gui;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.TestElementMetadata;
+import org.apache.jmeter.protocol.http.config.gui.GraphQLUrlConfigGui;
+import org.apache.jmeter.protocol.http.config.gui.UrlConfigGui;
+import org.apache.jmeter.util.JMeterUtils;
+
+/**
+ * GraphQL HTTP Sampler GUI which extends {@link HttpTestSampleGui} in order to provide more convenient UI elements for
+ * GraphQL query, variables and operationName.
+ */
+@TestElementMetadata(labelResource = "graphql_http_sampler_title")
+public class GraphQLHTTPSamplerGui extends HttpTestSampleGui {
+
+    private static final long serialVersionUID = 1L;
+
+    public GraphQLHTTPSamplerGui() {
+        super();
+    }
+
+    // Use this instead of getLabelResource() otherwise getDocAnchor() below does not work
+    @Override
+    public String getStaticLabel() {
+        return JMeterUtils.getResString("graphql_http_sampler_title"); // $NON-NLS-1$
+    }
+
+    @Override
+    public String getDocAnchor() {// reuse documentation
+        return super.getStaticLabel().replace(' ', '_'); //$NON-NLS-1$ //$NON-NLS-2$
+    }
+
+    /**
+     * {@inheritDoc}
+     * <P>
+     * Overridden to hide the HTML embedded resource handling section as GraphQL responses are always in JSON.
+     */
+    @Override
+    protected JPanel createEmbeddedRsrcPanel() {
+        final JPanel panel = super.createEmbeddedRsrcPanel();
+        // No need to consider embedded resources in HTML as the GraphQL responses are always in JSON.
+        panel.setVisible(false);
+        return panel;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <P>
+     * Overridden to create a {@link GraphQLUrlConfigGui} which extends {@link UrlConfigGui} for GraphQL specific UI elements.
+     */
+    @Override
+    protected UrlConfigGui createUrlConfigGui() {
+        final GraphQLUrlConfigGui configGui = new GraphQLUrlConfigGui();
+        configGui.setBorder(makeBorder());
+        return configGui;
+    }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java
index f7acf2a..0464568 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java
@@ -168,10 +168,52 @@ public class HttpTestSampleGui extends AbstractSamplerGui {
         setLayout(new BorderLayout(0, 5));
         setBorder(BorderFactory.createEmptyBorder());
 
+        JTabbedPane tabbedPane = createTabbedConfigPane();
+
+        JPanel wrapper = new JPanel(new BorderLayout());
+        wrapper.setBorder(makeBorder());
+        wrapper.add(makeTitlePanel(), BorderLayout.CENTER);
+
+        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, wrapper, tabbedPane);
+        splitPane.setBorder(BorderFactory.createEmptyBorder());
+        splitPane.setOneTouchExpandable(true);
+        add(splitPane);
+    }
+
+    /**
+     * Create the parameters configuration tabstrip which includes the Basic tab ({@link UrlConfigGui})
+     * and the Advanced tab by default.
+     * @return the parameters configuration tabstrip which includes the Basic tab ({@link UrlConfigGui})
+     *         and the Advanced tab by default
+     */
+    protected JTabbedPane createTabbedConfigPane() {
+        final JTabbedPane tabbedPane = new JTabbedPane();
+
         // URL CONFIG
-        urlConfigGui = new UrlConfigGui(true, true, true);
-        urlConfigGui.setBorder(makeBorder());
+        urlConfigGui = createUrlConfigGui();
+
+        tabbedPane.add(JMeterUtils
+                .getResString("web_testing_basic"), urlConfigGui);
+
+        // AdvancedPanel (embedded resources, source address and optional tasks)
+        final JPanel advancedPanel = createAdvancedConfigPanel();
+        tabbedPane.add(JMeterUtils
+                .getResString("web_testing_advanced"), advancedPanel);
 
+        return tabbedPane;
+    }
+
+    /**
+     * Create a {@link UrlConfigGui} which is used as the Basic tab in the parameters configuration tabstrip.
+     * @return a {@link UrlConfigGui} which is used as the Basic tab
+     */
+    protected UrlConfigGui createUrlConfigGui() {
+        final UrlConfigGui configGui = new UrlConfigGui(true, true, true);
+        configGui.setBorder(makeBorder());
+        return configGui;
+    }
+
+    private JPanel createAdvancedConfigPanel() {
         // HTTP request options
         JPanel httpOptions = new HorizontalPanel();
         httpOptions.add(getImplementationPanel());
@@ -190,21 +232,7 @@ public class HttpTestSampleGui extends AbstractSamplerGui {
         }
 
         advancedPanel.add(createOptionalTasksPanel());
-
-        JTabbedPane tabbedPane = new JTabbedPane();
-        tabbedPane.add(JMeterUtils
-                .getResString("web_testing_basic"), urlConfigGui);
-        tabbedPane.add(JMeterUtils
-                .getResString("web_testing_advanced"), advancedPanel);
-
-        JPanel wrapper = new JPanel(new BorderLayout());
-        wrapper.setBorder(makeBorder());
-        wrapper.add(makeTitlePanel(), BorderLayout.CENTER);
-
-        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, wrapper, tabbedPane);
-        splitPane.setBorder(BorderFactory.createEmptyBorder());
-        splitPane.setOneTouchExpandable(true);
-        add(splitPane);
+        return advancedPanel;
     }
 
     private JPanel getTimeOutPanel() {
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java
index 2881a3f..61d1af5 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java
@@ -33,12 +33,17 @@ import javax.xml.parsers.SAXParserFactory;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
 import org.apache.jmeter.protocol.http.config.MultipartUrlConfig;
+import org.apache.jmeter.protocol.http.config.gui.GraphQLUrlConfigGui;
+import org.apache.jmeter.protocol.http.control.Header;
+import org.apache.jmeter.protocol.http.control.gui.GraphQLHTTPSamplerGui;
 import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui;
 import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
 import org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory;
 import org.apache.jmeter.protocol.http.sampler.PostWriter;
 import org.apache.jmeter.protocol.http.util.ConversionUtils;
+import org.apache.jmeter.protocol.http.util.GraphQLRequestParamUtils;
 import org.apache.jmeter.protocol.http.util.HTTPConstants;
 import org.apache.jmeter.protocol.http.util.HTTPFileArg;
 import org.apache.jmeter.testelement.TestElement;
@@ -121,6 +126,45 @@ public class DefaultSamplerCreator extends AbstractSamplerCreator {
         if(arguments.getArgumentCount() == 1 && arguments.getArgument(0).getName().length()==0) {
             sampler.setPostBodyRaw(true);
         }
+
+        if (request.isDetectGraphQLRequest()) {
+            detectAndModifySamplerOnGraphQLRequest(sampler, request);
+        }
+    }
+
+    private void detectAndModifySamplerOnGraphQLRequest(final HTTPSamplerBase sampler, final HttpRequestHdr request) {
+        final String method = request.getMethod();
+        final Header header = request.getHeaderManager().getFirstHeaderNamed("Content-Type");
+        final boolean graphQLContentType = header != null
+                && GraphQLRequestParamUtils.isGraphQLContentType(header.getValue());
+
+        GraphQLRequestParams params = null;
+
+        if (HTTPConstants.POST.equals(method) && graphQLContentType) {
+            try {
+                byte[] postData = request.getRawPostData();
+                if (postData != null && postData.length > 0) {
+                    params = GraphQLRequestParamUtils.toGraphQLRequestParams(request.getRawPostData(),
+                            sampler.getContentEncoding());
+                }
+            } catch (Exception e) {
+                log.debug("Ignoring request, '{}' as it's not a valid GraphQL post data.");
+            }
+        } else if (HTTPConstants.GET.equals(method)) {
+            try {
+                params = GraphQLRequestParamUtils.toGraphQLRequestParams(sampler.getArguments(),
+                        sampler.getContentEncoding());
+            } catch (Exception e) {
+                log.debug("Ignoring request, '{}' as it does not valid GraphQL arguments.");
+            }
+        }
+
+        if (params != null) {
+            sampler.setProperty(TestElement.GUI_CLASS, GraphQLHTTPSamplerGui.class.getName());
+            sampler.setProperty(GraphQLUrlConfigGui.OPERATION_NAME, params.getOperationName());
+            sampler.setProperty(GraphQLUrlConfigGui.QUERY, params.getQuery());
+            sampler.setProperty(GraphQLUrlConfigGui.VARIABLES, params.getVariables());
+        }
     }
 
     /**
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java
index 961627b..abd69a8 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java
@@ -86,6 +86,8 @@ public class HttpRequestHdr {
 
     private String httpSampleNameFormat;
 
+    private boolean detectGraphQLRequest;
+
     public HttpRequestHdr() {
         this("", "");
     }
@@ -120,6 +122,22 @@ public class HttpRequestHdr {
     }
 
     /**
+     * Return true if automatic GraphQL Request detection is enabled.
+     * @return true if automatic GraphQL Request detection is enabled
+     */
+    public boolean isDetectGraphQLRequest() {
+        return detectGraphQLRequest;
+    }
+
+    /**
+     * Sets whether automatic GraphQL Request detection is enabled.
+     * @param detectGraphQLRequest whether automatic GraphQL Request detection is enabled
+     */
+    public void setDetectGraphQLRequest(boolean detectGraphQLRequest) {
+        this.detectGraphQLRequest = detectGraphQLRequest;
+    }
+
+    /**
      * Parses a http header from a stream.
      *
      * @param in
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java
index 5faf29b..37c1f47 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java
@@ -163,6 +163,8 @@ public class Proxy extends Thread {
 
         HttpRequestHdr request = new HttpRequestHdr(target.getPrefixHTTPSampleName(), httpSamplerName,
                 target.getHTTPSampleNamingMode(), target.getHttpSampleNameFormat());
+        request.setDetectGraphQLRequest(target.getDetectGraphQLRequest());
+
         SampleResult result = null;
         HeaderManager headers = null;
         HTTPSamplerBase sampler = null;
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
index 4341da2..642b943 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
@@ -136,6 +136,7 @@ public class ProxyControl extends GenericController implements NonTestElement {
     private static final String SAMPLER_REDIRECT_AUTOMATICALLY = "ProxyControlGui.sampler_redirect_automatically"; // $NON-NLS-1$
     private static final String SAMPLER_FOLLOW_REDIRECTS = "ProxyControlGui.sampler_follow_redirects"; // $NON-NLS-1$
     private static final String USE_KEEPALIVE = "ProxyControlGui.use_keepalive"; // $NON-NLS-1$
+    private static final String DETECT_GRAPHQL_REQUEST = "ProxyControlGui.detect_graphql_request"; // $NON-NLS-1$
     private static final String SAMPLER_DOWNLOAD_IMAGES = "ProxyControlGui.sampler_download_images"; // $NON-NLS-1$
     private static final String HTTP_SAMPLER_NAMING_MODE = "ProxyControlGui.proxy_http_sampler_naming_mode"; // $NON-NLS-1$
     private static final String HTTP_SAMPLER_FORMAT = "ProxyControlGui.proxy_http_sampler_format"; // $NON-NLS-1$
@@ -268,6 +269,8 @@ public class ProxyControl extends GenericController implements NonTestElement {
 
     private volatile boolean regexMatch = false;
 
+    private volatile boolean detectGraphQLRequest = false;
+
     private Set<Class<?>> addableInterfaces = new HashSet<>(
             Arrays.asList(Visualizer.class, ConfigElement.class,
                     Assertion.class, Timer.class, PreProcessor.class,
@@ -360,6 +363,11 @@ public class ProxyControl extends GenericController implements NonTestElement {
         setProperty(new BooleanProperty(USE_KEEPALIVE, b));
     }
 
+    public void setDetectGraphQLRequest(boolean b) {
+        detectGraphQLRequest = b;
+        setProperty(new BooleanProperty(DETECT_GRAPHQL_REQUEST, b));
+    }
+
     public void setSamplerDownloadImages(boolean b) {
         samplerDownloadImages = b;
         setProperty(new BooleanProperty(SAMPLER_DOWNLOAD_IMAGES, b));
@@ -460,6 +468,10 @@ public class ProxyControl extends GenericController implements NonTestElement {
         return getPropertyAsBoolean(USE_KEEPALIVE, true);
     }
 
+    public boolean getDetectGraphQLRequest() {
+        return getPropertyAsBoolean(DETECT_GRAPHQL_REQUEST, true);
+    }
+
     public boolean getSamplerDownloadImages() {
         return getPropertyAsBoolean(SAMPLER_DOWNLOAD_IMAGES, false);
     }
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
index a01dd58..d79428c 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
@@ -148,6 +148,11 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
      */
     private JCheckBox useKeepAlive;
 
+    /**
+     * Set/clear the Detect GraphQL Request box on the samplers (default is true)
+     */
+    private JCheckBox detectGraphQLRequest;
+
     /*
      * Use regexes to match the source data
      */
@@ -324,6 +329,7 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
             model.setSamplerRedirectAutomatically(samplerRedirectAutomatically.isSelected());
             model.setSamplerFollowRedirects(samplerFollowRedirects.isSelected());
             model.setUseKeepAlive(useKeepAlive.isSelected());
+            model.setDetectGraphQLRequest(detectGraphQLRequest.isSelected());
             model.setSamplerDownloadImages(samplerDownloadImages.isSelected());
             model.setHTTPSampleNamingMode(httpSampleNamingMode.getSelectedIndex());
             model.setDefaultEncoding(defaultEncoding.getText());
@@ -392,6 +398,7 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
         samplerRedirectAutomatically.setSelected(model.getSamplerRedirectAutomatically());
         samplerFollowRedirects.setSelected(model.getSamplerFollowRedirects());
         useKeepAlive.setSelected(model.getUseKeepalive());
+        detectGraphQLRequest.setSelected(model.getDetectGraphQLRequest());
         samplerDownloadImages.setSelected(model.getSamplerDownloadImages());
         httpSampleNamingMode.setSelectedIndex(model.getHTTPSampleNamingMode());
         prefixHTTPSampleName.setText(model.getPrefixHTTPSampleName());
@@ -757,6 +764,7 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
         testPlanPanel.add(createTestPlanContentPanel());
         testPlanPanel.add(Box.createVerticalStrut(5));
         testPlanPanel.add(createHTTPSamplerPanel());
+        testPlanPanel.add(createGraphQLHTTPSamplerPanel());
         tabbedPane.add(JMeterUtils
                 .getResString("proxy_test_plan_creation"), testPlanPanel);
 
@@ -993,6 +1001,20 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
 
         panel.add(labelSamplerType);
         panel.add(samplerTypeName, "growx, span");
+
+        return panel;
+    }
+
+    private JPanel createGraphQLHTTPSamplerPanel() {
+        detectGraphQLRequest = new JCheckBox(JMeterUtils.getResString("detect_graphql_request")); // $NON-NLS-1$
+        detectGraphQLRequest.setSelected(true);
+        detectGraphQLRequest.addActionListener(this);
+        detectGraphQLRequest.setActionCommand(ENABLE_RESTART);
+
+        JPanel panel = new JPanel(new MigLayout("fillx, wrap 3"));
+        panel.setBorder(BorderFactory.createTitledBorder(JMeterUtils.getResString("proxy_sampler_graphql_settings"))); // $NON-NLS-1$
+        panel.add(detectGraphQLRequest);
+
         return panel;
     }
 
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/util/GraphQLRequestParamUtils.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/util/GraphQLRequestParamUtils.java
new file mode 100644
index 0000000..6844722
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/util/GraphQLRequestParamUtils.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.entity.ContentType;
+import org.apache.jmeter.config.Argument;
+import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
+import org.apache.jmeter.testelement.property.JMeterProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Utilities to (de)serialize GraphQL request parameters.
+ */
+public final class GraphQLRequestParamUtils {
+
+    private static Logger log = LoggerFactory.getLogger(GraphQLRequestParamUtils.class);
+
+    private static Pattern WHITESPACES_PATTERN = Pattern.compile("\\p{Space}+");
+
+    private GraphQLRequestParamUtils() {
+    }
+
+    /**
+     * Return true if the content type is GraphQL content type (i.e. 'application/json').
+     * @param contentType Content-Type value
+     * @return true if the content type is GraphQL content type
+     */
+    public static boolean isGraphQLContentType(final String contentType) {
+        if (StringUtils.isEmpty(contentType)) {
+            return false;
+        }
+        final ContentType type = ContentType.parse(contentType);
+        return ContentType.APPLICATION_JSON.getMimeType().equals(type.getMimeType());
+    }
+
+    /**
+     * Convert the GraphQL request parameters input data to an HTTP POST body string.
+     * @param params GraphQL request parameter input data
+     * @return an HTTP POST body string converted from the GraphQL request parameters input data
+     * @throws RuntimeException if JSON serialization fails for some reason due to any runtime environment issues
+     */
+    public static String toPostBodyString(final GraphQLRequestParams params) {
+        final ObjectMapper mapper = new ObjectMapper();
+        final ObjectNode postBodyJson = mapper.createObjectNode();
+        postBodyJson.put("operationName", StringUtils.trimToNull(params.getOperationName()));
+
+        if (StringUtils.isNotBlank(params.getVariables())) {
+            try {
+                final ObjectNode variablesJson = mapper.readValue(params.getVariables(), ObjectNode.class);
+                postBodyJson.set("variables", variablesJson);
+            } catch (JsonProcessingException e) {
+                log.error("Ignoring the GraphQL query variables content due to the syntax error: {}",
+                        e.getLocalizedMessage());
+            }
+        }
+
+        postBodyJson.put("query", StringUtils.trim(params.getQuery()));
+
+        try {
+            return mapper.writeValueAsString(postBodyJson);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException("Cannot serialize JSON for POST body string", e);
+        }
+    }
+
+    /**
+     * Convert the GraphQL Query input string into an HTTP GET request parameter value.
+     * @param query the GraphQL Query input string
+     * @return an HTTP GET request parameter value converted from the GraphQL Query input string
+     */
+    public static String queryToGetParamValue(final String query) {
+        return RegExUtils.replaceAll(StringUtils.trim(query), WHITESPACES_PATTERN, " ");
+    }
+
+    /**
+     * Convert the GraphQL Variables JSON input string into an HTTP GET request parameter value.
+     * @param variables the GraphQL Variables JSON input string
+     * @return an HTTP GET request parameter value converted from the GraphQL Variables JSON input string
+     */
+    public static String variablesToGetParamValue(final String variables) {
+        final ObjectMapper mapper = new ObjectMapper();
+
+        try {
+            final ObjectNode variablesJson = mapper.readValue(variables, ObjectNode.class);
+            return mapper.writeValueAsString(variablesJson);
+        } catch (JsonProcessingException e) {
+            log.error("Ignoring the GraphQL query variables content due to the syntax error: {}",
+                    e.getLocalizedMessage());
+        }
+
+        return null;
+    }
+
+    /**
+     * Parse {@code postData} and convert it to a {@link GraphQLRequestParams} object if it is a valid GraphQL post data.
+     * @param postData post data
+     * @param contentEncoding content encoding
+     * @return a converted {@link GraphQLRequestParams} object form the {@code postData}
+     * @throws IllegalArgumentException if {@code postData} is not a GraphQL post JSON data or not a valid JSON
+     * @throws JsonProcessingException if it fails to serialize a parsed JSON object to string
+     * @throws UnsupportedEncodingException if it fails to decode parameter value
+     */
+    public static GraphQLRequestParams toGraphQLRequestParams(byte[] postData, final String contentEncoding)
+            throws IllegalArgumentException, JsonProcessingException, UnsupportedEncodingException {
+        final String encoding = StringUtils.isNotEmpty(contentEncoding) ? contentEncoding
+                : EncoderCache.URL_ARGUMENT_ENCODING;
+
+        final ObjectMapper mapper = new ObjectMapper();
+        ObjectNode data;
+
+        try (InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(postData), encoding)) {
+            data = mapper.readValue(reader, ObjectNode.class);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Invalid json data: " + e.getLocalizedMessage());
+        }
+
+        String operationName = null;
+        String query = null;
+        String variables = null;
+
+        final JsonNode operationNameNode = data.has("operationName") ? data.get("operationName") : null;
+        if (operationNameNode != null) {
+            operationName = getJsonNodeTextContent(operationNameNode, true);
+        }
+
+        if (!data.has("query")) {
+            throw new IllegalArgumentException("Not a valid GraphQL query.");
+        }
+        final JsonNode queryNode = data.get("query");
+        query = getJsonNodeTextContent(queryNode, false);
+        final String trimmedQuery = StringUtils.trim(query);
+        if (!StringUtils.startsWith(trimmedQuery, "query") && !StringUtils.startsWith(trimmedQuery, "mutation")) {
+            throw new IllegalArgumentException("Not a valid GraphQL query.");
+        }
+
+        final JsonNode variablesNode = data.has("variables") ? data.get("variables") : null;
+        if (variablesNode != null) {
+            final JsonNodeType nodeType = variablesNode.getNodeType();
+            if (nodeType != JsonNodeType.NULL) {
+                if (nodeType == JsonNodeType.OBJECT) {
+                    variables = mapper.writeValueAsString(variablesNode);
+                } else {
+                    throw new IllegalArgumentException("Not a valid object node for GraphQL variables.");
+                }
+            }
+        }
+
+        return new GraphQLRequestParams(operationName, query, variables);
+    }
+
+    /**
+     * Parse {@code arguments} and convert it to a {@link GraphQLRequestParams} object if it has valid GraphQL HTTP arguments.
+     * @param arguments arguments
+     * @param contentEncoding content encoding
+     * @return a converted {@link GraphQLRequestParams} object form the {@code arguments}
+     * @throws IllegalArgumentException if {@code arguments} does not contain valid GraphQL request arguments
+     * @throws UnsupportedEncodingException if it fails to decode parameter value
+     */
+    public static GraphQLRequestParams toGraphQLRequestParams(final Arguments arguments, final String contentEncoding)
+            throws IllegalArgumentException, UnsupportedEncodingException {
+        final String encoding = StringUtils.isNotEmpty(contentEncoding) ? contentEncoding
+                : EncoderCache.URL_ARGUMENT_ENCODING;
+
+        String operationName = null;
+        String query = null;
+        String variables = null;
+
+        for (JMeterProperty prop : arguments) {
+            final Argument arg = (Argument) prop.getObjectValue();
+            if (!(arg instanceof HTTPArgument)) {
+                continue;
+            }
+
+            final String name = arg.getName();
+            final String metadata = arg.getMetaData();
+            final String value = StringUtils.trimToNull(arg.getValue());
+
+            if ("=".equals(metadata) && value != null) {
+                final boolean alwaysEncoded = ((HTTPArgument) arg).isAlwaysEncoded();
+
+                if ("operationName".equals(name)) {
+                    operationName = alwaysEncoded ? value : URLDecoder.decode(value, encoding);
+                } else if ("query".equals(name)) {
+                    query = alwaysEncoded ? value : URLDecoder.decode(value, encoding);
+                } else if ("variables".equals(name)) {
+                    variables = alwaysEncoded ? value : URLDecoder.decode(value, encoding);
+                }
+            }
+        }
+
+        if (StringUtils.isEmpty(query)
+                || (!StringUtils.startsWith(query, "query") && !StringUtils.startsWith(query, "mutation"))) {
+            throw new IllegalArgumentException("Not a valid GraphQL query.");
+        }
+
+        if (StringUtils.isNotEmpty(variables)) {
+            if (!StringUtils.startsWith(variables, "{") || !StringUtils.endsWith(variables, "}")) {
+                throw new IllegalArgumentException("Not a valid object node for GraphQL variables.");
+            }
+        }
+
+        return new GraphQLRequestParams(operationName, query, variables);
+    }
+
+    private static String getJsonNodeTextContent(final JsonNode jsonNode, final boolean nullable) throws IllegalArgumentException {
+        final JsonNodeType nodeType = jsonNode.getNodeType();
+
+        if (nodeType == JsonNodeType.NULL) {
+            if (nullable) {
+                return null;
+            }
+
+            throw new IllegalArgumentException("Not a non-null value node.");
+        }
+
+        if (nodeType == JsonNodeType.STRING) {
+            return jsonNode.asText();
+        }
+
+        throw new IllegalArgumentException("Not a string value node.");
+    }
+}
diff --git a/src/protocol/http/src/main/resources/org/apache/jmeter/protocol/http/sampler/GraphQLHTTPSamplerResources.properties b/src/protocol/http/src/main/resources/org/apache/jmeter/protocol/http/sampler/GraphQLHTTPSamplerResources.properties
new file mode 100644
index 0000000..27951e8
--- /dev/null
+++ b/src/protocol/http/src/main/resources/org/apache/jmeter/protocol/http/sampler/GraphQLHTTPSamplerResources.properties
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+displayName=GraphQL HTTP Request
+defaults.displayName=Default Test Values
+method.displayName=Method
+method.shortDescription=Method
+operationName.displayName=Operation Name
+operationName.shortDescription=Operation Name
+query.displayName=Query
+query.shortDescription=Query or Mutation
+variables.displayName=Variables
+variables.shortDescription=Variables
diff --git a/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/util/TestGraphQLRequestParamUtils.java b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/util/TestGraphQLRequestParamUtils.java
new file mode 100644
index 0000000..8316e68
--- /dev/null
+++ b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/util/TestGraphQLRequestParamUtils.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.github.jknack.handlebars.internal.lang3.StringUtils;
+
+public class TestGraphQLRequestParamUtils {
+
+    private static final String OPERATION_NAME = "";
+
+    private static final String QUERY =
+            "query($id: ID!) {\n"
+            + "  droid(id: $id) {\n"
+            + "    id\n"
+            + "    name\n"
+            + "    friends {\n"
+            + "      id\n"
+            + "      name\n"
+            + "      appearsIn\n"
+            + "    }\n"
+            + "  }\n"
+            + "}\n";
+
+    private static final String VARIABLES =
+            "{\n"
+            + "  \"id\": \"2001\"\n"
+            + "}\n";
+
+    private static final String EXPECTED_QUERY_GET_PARAM_VALUE =
+            "query($id: ID!) { droid(id: $id) { id name friends { id name appearsIn } } }";
+
+    private static final String EXPECTED_VARIABLES_GET_PARAM_VALUE = "{\"id\":\"2001\"}";
+
+    private static final String EXPECTED_POST_BODY =
+            "{"
+            + "\"operationName\":null,"
+            + "\"variables\":" + EXPECTED_VARIABLES_GET_PARAM_VALUE + ","
+            + "\"query\":\"" + StringUtils.replace(QUERY.trim(), "\n", "\\n") + "\""
+            + "}";
+
+    private GraphQLRequestParams params;
+
+    @BeforeEach
+    public void setUp() {
+        params = new GraphQLRequestParams(OPERATION_NAME, QUERY, VARIABLES);
+    }
+
+    @Test
+    public void testIsGraphQLContentType() throws Exception {
+        assertTrue(GraphQLRequestParamUtils.isGraphQLContentType("application/json"));
+        assertTrue(GraphQLRequestParamUtils.isGraphQLContentType("application/json;charset=utf-8"));
+        assertTrue(GraphQLRequestParamUtils.isGraphQLContentType("application/json; charset=utf-8"));
+
+        assertFalse(GraphQLRequestParamUtils.isGraphQLContentType("application/vnd.api+json"));
+        assertFalse(GraphQLRequestParamUtils.isGraphQLContentType("application/json-patch+json"));
+        assertFalse(GraphQLRequestParamUtils.isGraphQLContentType(""));
+        assertFalse(GraphQLRequestParamUtils.isGraphQLContentType(null));
+    }
+
+    @Test
+    public void testToPostBodyString() throws Exception {
+        assertEquals(EXPECTED_POST_BODY, GraphQLRequestParamUtils.toPostBodyString(params));
+    }
+
+    @Test
+    public void testQueryToGetParamValue() throws Exception {
+        assertEquals(EXPECTED_QUERY_GET_PARAM_VALUE, GraphQLRequestParamUtils.queryToGetParamValue(params.getQuery()));
+    }
+
+    @Test
+    public void testVariablesToGetParamValue() throws Exception {
+        assertEquals(EXPECTED_VARIABLES_GET_PARAM_VALUE,
+                GraphQLRequestParamUtils.variablesToGetParamValue(params.getVariables()));
+    }
+
+    @Test
+    public void testToGraphQLRequestParamsWithPostData() throws Exception {
+        GraphQLRequestParams params = GraphQLRequestParamUtils
+                .toGraphQLRequestParams(EXPECTED_POST_BODY.getBytes(StandardCharsets.UTF_8), null);
+        assertNull(params.getOperationName());
+        assertEquals(QUERY.trim(), params.getQuery());
+        assertEquals(EXPECTED_VARIABLES_GET_PARAM_VALUE, params.getVariables());
+
+        params = GraphQLRequestParamUtils.toGraphQLRequestParams(
+                "{\"operationName\":\"op1\",\"variables\":{\"id\":123},\"query\":\"query { droid { id }}\"}"
+                        .getBytes(StandardCharsets.UTF_8),
+                null);
+        assertEquals("op1", params.getOperationName());
+        assertEquals("query { droid { id }}", params.getQuery());
+        assertEquals("{\"id\":123}", params.getVariables());
+
+        try {
+            params = GraphQLRequestParamUtils.toGraphQLRequestParams("".getBytes(StandardCharsets.UTF_8), null);
+            fail("Should have failed due to invalid json data.");
+        } catch (IllegalArgumentException ignore) {
+        }
+
+        try {
+            params = GraphQLRequestParamUtils.toGraphQLRequestParams("{}".getBytes(StandardCharsets.UTF_8), null);
+            fail("Should have failed due to invalid json data.");
+        } catch (IllegalArgumentException ignore) {
+        }
+
+        try {
+            params = GraphQLRequestParamUtils
+                    .toGraphQLRequestParams("{\"query\":\"select * from emp\"}".getBytes(StandardCharsets.UTF_8), null);
+            fail("Should have failed due to invalid graph query param.");
+        } catch (IllegalArgumentException ignore) {
+        }
+
+        try {
+            params = GraphQLRequestParamUtils
+                    .toGraphQLRequestParams("{\"operationName\":{\"id\":123},\"query\":\"query { droid { id }}\"}"
+                            .getBytes(StandardCharsets.UTF_8), null);
+            fail("Should have failed due to invalid graph operationName type.");
+        } catch (IllegalArgumentException ignore) {
+        }
+
+        try {
+            params = GraphQLRequestParamUtils.toGraphQLRequestParams(
+                    "{\"variables\":\"r2d2\",\"query\":\"query { droid { id }}\"}".getBytes(StandardCharsets.UTF_8),
+                    null);
+            fail("Should have failed due to invalid graph variables type.");
+        } catch (IllegalArgumentException ignore) {
+        }
+    }
+
+    @Test
+    public void testToGraphQLRequestParamsWithHttpArguments() throws Exception {
+        Arguments args = new Arguments();
+        args.addArgument(new HTTPArgument("query", "query { droid { id }}", "=", false));
+        GraphQLRequestParams params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+        assertNull(params.getOperationName());
+        assertEquals("query { droid { id }}", params.getQuery());
+        assertNull(params.getVariables());
+
+        args = new Arguments();
+        args.addArgument(new HTTPArgument("operationName", "op1", "=", false));
+        args.addArgument(new HTTPArgument("query", "query { droid { id }}", "=", false));
+        args.addArgument(new HTTPArgument("variables", "{\"id\":123}", "=", false));
+        params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+        assertEquals("op1", params.getOperationName());
+        assertEquals("query { droid { id }}", params.getQuery());
+        assertEquals("{\"id\":123}", params.getVariables());
+
+        args = new Arguments();
+        args.addArgument(new HTTPArgument("query", "query+%7B+droid+%7B+id+%7D%7D", "=", true));
+        params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+        assertNull(params.getOperationName());
+        assertEquals("query { droid { id }}", params.getQuery());
+        assertNull(params.getVariables());
+
+        args = new Arguments();
+        args.addArgument(new HTTPArgument("query", "query%20%7B%20droid%20%7B%20id%20%7D%7D", "=", true));
+        params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+        assertNull(params.getOperationName());
+        assertEquals("query { droid { id }}", params.getQuery());
+        assertNull(params.getVariables());
+
+        try {
+            args = new Arguments();
+            params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+            fail("Should have failed due to missing GraphQL parameters.");
+        } catch (IllegalArgumentException ignore) {
+        }
+
+        try {
+            args = new Arguments();
+            args.addArgument(new HTTPArgument("query", "select * from emp", "=", false));
+            params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+            fail("Should have failed due to invalid graph query param.");
+        } catch (IllegalArgumentException ignore) {
+        }
+
+        try {
+            args = new Arguments();
+            args.addArgument(new HTTPArgument("query", "query { droid { id }}", "=", false));
+            args.addArgument(new HTTPArgument("variables", "r2d2", "=", false));
+            params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+            fail("Should have failed due to invalid graph query param.");
+        } catch (IllegalArgumentException ignore) {
+        }
+    }
+}
diff --git a/xdocs/changes.xml b/xdocs/changes.xml
index 4fb8339..d6a24f4 100644
--- a/xdocs/changes.xml
+++ b/xdocs/changes.xml
@@ -83,6 +83,7 @@ applications when JMeter is starting up.</p>
 <ul>
     <li><bug>53848</bug><bug>63527</bug>Implement a new setting to allow the exclusion of embedded URLs</li>
     <li><bug>64696</bug><pr>571</pr><pr>595</pr>Freestyle format for names in (Default)SamplerCreater. Based on a patch by Vincent Daburon (vdaburon at gmail.com)</li>
+    <li><bug>64752</bug>Add GraphQL/HTTP Request Sampler. Contributed by woonsan.</li>
 </ul>
 
 <h3>Other samplers</h3>
diff --git a/xdocs/demos/SimpleGraphQLTestPlan.jmx b/xdocs/demos/SimpleGraphQLTestPlan.jmx
new file mode 100644
index 0000000..7d3d49f
--- /dev/null
+++ b/xdocs/demos/SimpleGraphQLTestPlan.jmx
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.3.1-SNAPSHOT 790e46c">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Simple GraphQL HTTP Request Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments">Simple GraphQL HTTP Request Test Plan for demonstration purpose, querying data from a demo GraphQL server</stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
+        <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+          <collectionProp name="Arguments.arguments"/>
+        </elementProp>
+        <stringProp name="HTTPSampler.domain">localhost</stringProp>
+        <stringProp name="HTTPSampler.port">8080</stringProp>
+        <stringProp name="HTTPSampler.protocol">http</stringProp>
+        <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+        <stringProp name="HTTPSampler.path"></stringProp>
+        <stringProp name="HTTPSampler.concurrentPool">6</stringProp>
+        <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+        <stringProp name="HTTPSampler.response_timeout"></stringProp>
+      </ConfigTestElement>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">1</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="GraphQLHTTPSamplerGui" testclass="HTTPSamplerProxy" testname="GraphQL HTTP Request to get the favorite droid" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument" enabled="true">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{&quot;operationName&quot;:null,&quot;variables&quot;:{&quot;id&quot;:&quot;2001&quot;},&quot;query&quot;:&quot;query($id: ID!) {\n  droid(id: $id) {\n    id\n    name\n    friends {\n      id\n      name\n      appearsIn\n    }\n  }\n}&quot;}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+                <boolProp name="HTTPArgument.use_equals">true</boolProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain"></stringProp>
+          <stringProp name="HTTPSampler.port"></stringProp>
+          <stringProp name="HTTPSampler.protocol"></stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/graphql</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <stringProp name="GraphQLHTTPSampler.operationName"></stringProp>
+          <stringProp name="GraphQLHTTPSampler.query">query($id: ID!) {
+  droid(id: $id) {
+    id
+    name
+    friends {
+      id
+      name
+      appearsIn
+    }
+  }
+}</stringProp>
+          <stringProp name="GraphQLHTTPSampler.variables">{
+  &quot;id&quot;: &quot;2001&quot;
+}</stringProp>
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          <stringProp name="HTTPSampler.embedded_url_exclude_re"></stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>true</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <sentBytes>true</sentBytes>
+            <url>true</url>
+            <threadCounts>true</threadCounts>
+            <idleTime>true</idleTime>
+            <connectTime>true</connectTime>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/xdocs/images/screenshots/graphql-http-request-vars.png b/xdocs/images/screenshots/graphql-http-request-vars.png
new file mode 100644
index 0000000..b6e0d9e
Binary files /dev/null and b/xdocs/images/screenshots/graphql-http-request-vars.png differ
diff --git a/xdocs/images/screenshots/graphql-http-request.png b/xdocs/images/screenshots/graphql-http-request.png
new file mode 100644
index 0000000..50a3609
Binary files /dev/null and b/xdocs/images/screenshots/graphql-http-request.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 9e8cf07..b241ac5 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -130,7 +130,7 @@ Latency is set to the time it takes to login.
         them.  This can save you time if you have a lot of HTTP requests or requests with many
         parameters.</p>
 
-        <p><b>There are two different test elements used to define the samplers:</b></p>
+        <p><b>There are three different test elements used to define the samplers:</b></p>
         <dl>
         <dt>AJP/1.3 Sampler</dt><dd>uses the Tomcat mod_jk protocol (allows testing of Tomcat in AJP mode without needing Apache httpd)
         The AJP Sampler does not support multiple file upload; only the first file will be used.
@@ -143,6 +143,17 @@ Latency is set to the time it takes to login.
             <dt>Blank Value</dt><dd>does not set implementation on HTTP Samplers, so relies on HTTP Request Defaults if present or on <code>jmeter.httpsampler</code> property defined in <code>jmeter.properties</code></dd>
           </dl>
         </dd>
+        <dt>GraphQL HTTP Request</dt><dd>this is a GUI variation of the <b>HTTP Request</b> to provide more convenient UI elements
+        to view or edit GraphQL <b>Query</b>, <b>Variables</b> and <b>Operation Name</b>, while converting them into HTTP Arguments automatically under the hood
+        using the same sampler.
+        This hides or customizes the following UI elements as they are less convenient for or irrelevant to GraphQL over HTTP/HTTPS requests:
+        <ul>
+          <li><b>Method</b>: Only POST and GET methods are available conforming the GraphQL over HTTP specification. POST method is selected by default.</li>
+          <li><b>Parameters</b> and <b>Post Body</b> tabs: you may view or edit parameter content through Query, Variables and Operation Name UI elements instead.</li>
+          <li><b>File Upload</b> tab: irrelevant to GraphQL queries.</li>
+          <li><b>Embedded Resources from HTML Files</b> section in the Advanced tab: irrelevant in GraphQL JSON responses.</li>
+        </ul>
+        </dd>
         </dl>
          <p>The Java HTTP implementation has some limitations:</p>
          <ul>
@@ -201,6 +212,8 @@ https.default.protocol=SSLv3
         for additional configuration steps.</p>
 </description>
 <figure width="951" height="284" image="http-request-advanced-tab.png">HTTP Request Advanced config fields</figure>
+<figure width="950" height="618" image="graphql-http-request.png">Screenshot of Control-Panel of GraphQL HTTP Request</figure>
+<figure width="950" height="618" image="graphql-http-request-vars.png">Variables field for GraphQL HTTP Request</figure>
 
 <properties>
         <property name="Name" required="No">Descriptive name for this sampler that is shown in the tree.</property>
@@ -363,6 +376,19 @@ and send HTTP/HTTPS requests for all images, Java applets, JavaScript files, CSS
         If the property <code>httpclient.localaddress</code> is defined, that is used for all HttpClient requests.
         </property>
 </properties>
+      <p>The following parameters are available only for <b>GraphQL HTTP Request</b>:</p>
+      <properties>
+        <property name="Query" required="Yes">
+          GraphQL query (or mutation) statement.
+        </property>
+        <property name="Variables" required="No">
+          GraphQL query (or mutation) variables in a valid JSON string.
+          <b>Note</b>: If the input string is not a valid JSON string, this will be ignored with an ERROR log.
+        </property>
+        <property name="Operation Name" required="No">
+          Optional GraphQL operation name when making a request for multi-operation documents.
+        </property>
+      </properties>
 <note>
 When using Automatic Redirection, cookies are only sent for the initial URL.
 This can cause unexpected behaviour for web-sites that redirect to a local server.


Mime
View raw message