This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/master by this push:
     new 6a31675855 Implement probe-full-json feature for xdebug plugin (#12460)
6a31675855 is described below

commit 6a31675855358fafd09053d639433fddd5304750
Author: Brian Neradt <[email protected]>
AuthorDate: Tue Aug 19 13:28:00 2025 -0500

    Implement probe-full-json feature for xdebug plugin (#12460)
    
    * Add an xdebug probe test
    
    * xdebug: use header files; xdebug namespace
    
    * Implement probe-full-json feature for xdebug plugin
    
    The probe-full-json directive emits the same data as probe but in a
    complete JSON object containing nodes for client/server request headers,
    response body, and client/server response headers.
---
 doc/admin-guide/plugins/xdebug.en.rst              |  69 ++++++++-
 plugins/xdebug/CMakeLists.txt                      |   2 +-
 plugins/xdebug/xdebug.cc                           | 168 +++++++++++----------
 plugins/xdebug/xdebug_headers.cc                   | 139 +++++++++++++----
 plugins/xdebug/xdebug_headers.h                    |  83 ++++++++++
 plugins/xdebug/xdebug_transforms.cc                |  58 ++++++-
 plugins/xdebug/xdebug_transforms.h                 |  51 +++++++
 plugins/xdebug/xdebug_types.h                      |  58 +++++++
 .../pluginTest/xdebug/x_probe/x_probe.replay.yaml  |  58 +++++++
 .../pluginTest/xdebug/x_probe/x_probe.test.py      |  80 ++++++++++
 .../xdebug/x_probe_full_json/gold/jq.gold          |   3 +
 .../x_probe_full_json.replay.yaml                  |  57 +++++++
 .../x_probe_full_json/x_probe_full_json.test.py    |  98 ++++++++++++
 13 files changed, 808 insertions(+), 116 deletions(-)

diff --git a/doc/admin-guide/plugins/xdebug.en.rst 
b/doc/admin-guide/plugins/xdebug.en.rst
index 987477cbb8..8946f03959 100644
--- a/doc/admin-guide/plugins/xdebug.en.rst
+++ b/doc/admin-guide/plugins/xdebug.en.rst
@@ -53,9 +53,18 @@ selectively by passing header names to ``--enable`` option.
     --enable=x-remap,x-cache
 
 This enables ``X-Remap`` and ``X-Cache``. If a client's request has
-``X-Debug: x-remap, x-cache, probe``, XDebug will only injects ``X-Reamp`` and
+``X-Debug: x-remap, x-cache, probe``, XDebug will only inject ``X-Remap`` and
 ``X-Cache``.
 
+To enable the JSON transaction header probe functionality:
+
+::
+
+    --enable=probe-full-json
+
+This allows clients to request ``X-Debug: probe-full-json`` to receive request
+and response header information in a structured JSON format.
+
 
 Debugging Headers
 =================
@@ -87,6 +96,64 @@ Probe
     - Response Headers from Proxy B -> Proxy A
     - Response Headers from Proxy A -> Client
 
+Probe-Full-JSON
+    Similar to `Probe` but formats the output as a complete JSON object
+    containing request and response headers. The response body is modified to
+    include client request headers, proxy request headers, the original server
+    response body, server response headers, and proxy response headers in a
+    structured JSON format. In contrast to Probe, the response content with
+    this feature is parsable with JSON parsing tools like ``jq``. Because the
+    body is altered, it disables writing to cache and changes the Content-Type
+    to ``application/json``.
+
+    JSON Nodes:
+
+    - ``client-request``: Headers from the client to the proxy.
+    - ``proxy-request``: Headers from the proxy to the origin server (if 
applicable).
+    - ``server-body``: The original response body content from the origin 
server.
+    - ``server-response``: Headers from the origin server to the proxy.
+    - ``proxy-response``: Headers from the proxy to the client.
+
+    Here's an example of the JSON output from the `x_probe_full_json` test::
+
+        $ curl -s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: 
probe-full-json" http://127.0.0.1:61003/test | jq
+        {
+            "client-request": {
+                "start-line": "GET http://127.0.0.1:61000/test HTTP/1.1",
+                "uuid": "1",
+                "host": "127.0.0.1:61000",
+                "x-request": "from-client"
+            },
+            "proxy-request": {
+                "start-line": "GET /test HTTP/1.1",
+                "uuid": "1",
+                "host": "127.0.0.1:61000",
+                "x-request": "from-client",
+                "Client-ip": "127.0.0.1",
+                "X-Forwarded-For": "127.0.0.1",
+                "Via": "http/1.1 
traffic_server[f47ffc16-0a20-441e-b17d-6e3cb044e025] 
(ApacheTrafficServer/10.2.0)"
+            },
+            "server-body": "Original server response",
+            "server-response": {
+                "start-line": "HTTP/1.1 200 ",
+                "content-type": "text/html",
+                "content-length": "24",
+                "x-response": "from-origin",
+                "Date": "Tue, 19 Aug 2025 15:02:07 GMT"
+            },
+            "proxy-response": {
+                "start-line": "HTTP/1.1 200 OK",
+                "content-type": "application/json",
+                "x-response": "from-origin",
+                "Date": "Tue, 19 Aug 2025 15:02:07 GMT",
+                "Age": "0",
+                "Transfer-Encoding": "chunked",
+                "Connection": "keep-alive",
+                "Server": "ATS/10.2.0",
+                "X-Original-Content-Type": "text/html"
+            }
+        }
+
 X-Cache-Key
     The ``X-Cache-Key`` header contains the URL that identifies the HTTP 
object in the
     Traffic Server cache. This header is particularly useful if a custom cache
diff --git a/plugins/xdebug/CMakeLists.txt b/plugins/xdebug/CMakeLists.txt
index 17ae23e7de..506617617c 100644
--- a/plugins/xdebug/CMakeLists.txt
+++ b/plugins/xdebug/CMakeLists.txt
@@ -15,6 +15,6 @@
 #
 #######################
 
-add_atsplugin(xdebug xdebug.cc)
+add_atsplugin(xdebug xdebug.cc xdebug_headers.cc xdebug_transforms.cc)
 target_link_libraries(xdebug PRIVATE libswoc::libswoc)
 verify_global_plugin(xdebug)
diff --git a/plugins/xdebug/xdebug.cc b/plugins/xdebug/xdebug.cc
index ecc36ec8f5..ea97c6b805 100644
--- a/plugins/xdebug/xdebug.cc
+++ b/plugins/xdebug/xdebug.cc
@@ -16,13 +16,12 @@
  * limitations under the License.
  */
 
+#include <climits>
 #include <cstdlib>
 #include <cstdio>
 #include <cstdio>
 #include <strings.h>
-#include <sstream>
 #include <cstring>
-#include <atomic>
 #include <memory>
 #include <getopt.h>
 #include <cstdint>
@@ -37,37 +36,21 @@
 #include "swoc/TextView.h"
 #include "tscpp/api/Cleanup.h"
 
-namespace
-{
-DbgCtl dbg_ctl{"xdebug"};
-
-struct BodyBuilder {
-  atscppapi::TSContUniqPtr     transform_connp;
-  atscppapi::TSIOBufferUniqPtr output_buffer;
-  // It's important that output_reader comes after output_buffer so it will be 
deleted first.
-  atscppapi::TSIOBufferReaderUniqPtr output_reader;
-  TSVIO                              output_vio    = nullptr;
-  bool                               wrote_prebody = false;
-  bool                               wrote_body    = false;
-  bool                               hdr_ready     = false;
-  std::atomic_flag                   wrote_postbody;
-
-  int64_t nbytes = 0;
-};
-
-struct XDebugTxnAuxData {
-  std::unique_ptr<BodyBuilder> body_builder;
-  unsigned                     xheaders = 0;
-};
+#include "xdebug_types.h"
+#include "xdebug_headers.h"
+#include "xdebug_transforms.h"
 
-atscppapi::TxnAuxMgrData mgrData;
-
-using AuxDataMgr = atscppapi::TxnAuxDataMgr<XDebugTxnAuxData, mgrData>;
+namespace xdebug
+{
 
+namespace
+{
+  DbgCtl dbg_ctl{"xdebug"};
+  DbgCtl dbg_ctl_xform{"xdebug_transform"};
 } // end anonymous namespace
 
-#include "xdebug_headers.cc"
-#include "xdebug_transforms.cc"
+// Definition of the auxiliary data manager
+atscppapi::TxnAuxMgrData mgrData;
 
 static struct {
   const char *str;
@@ -75,57 +58,60 @@ static struct {
 } xDebugHeader = {nullptr, 0};
 
 enum {
-  XHEADER_X_CACHE_KEY      = 1u << 2,
-  XHEADER_X_MILESTONES     = 1u << 3,
-  XHEADER_X_CACHE          = 1u << 4,
-  XHEADER_X_GENERATION     = 1u << 5,
-  XHEADER_X_TRANSACTION_ID = 1u << 6,
-  XHEADER_X_DUMP_HEADERS   = 1u << 7,
-  XHEADER_X_REMAP          = 1u << 8,
-  XHEADER_X_PROBE_HEADERS  = 1u << 9,
-  XHEADER_X_PSELECT_KEY    = 1u << 10,
-  XHEADER_X_CACHE_INFO     = 1u << 11,
-  XHEADER_X_EFFECTIVE_URL  = 1u << 12,
-  XHEADER_VIA              = 1u << 13,
-  XHEADER_DIAGS            = 1u << 14,
-  XHEADER_ALL              = UINT_MAX
+  XHEADER_X_CACHE_KEY       = 1u << 2,
+  XHEADER_X_MILESTONES      = 1u << 3,
+  XHEADER_X_CACHE           = 1u << 4,
+  XHEADER_X_GENERATION      = 1u << 5,
+  XHEADER_X_TRANSACTION_ID  = 1u << 6,
+  XHEADER_X_DUMP_HEADERS    = 1u << 7,
+  XHEADER_X_REMAP           = 1u << 8,
+  XHEADER_X_PROBE_HEADERS   = 1u << 9,
+  XHEADER_X_PSELECT_KEY     = 1u << 10,
+  XHEADER_X_CACHE_INFO      = 1u << 11,
+  XHEADER_X_EFFECTIVE_URL   = 1u << 12,
+  XHEADER_VIA               = 1u << 13,
+  XHEADER_DIAGS             = 1u << 14,
+  XHEADER_X_PROBE_FULL_JSON = 1u << 15,
+  XHEADER_ALL               = UINT_MAX
 };
 
 static unsigned int allowedHeaders = 0;
 
-constexpr std::string_view HEADER_NAME_X_CACHE_KEY      = "x-cache-key";
-constexpr std::string_view HEADER_NAME_X_MILESTONES     = "x-milestones";
-constexpr std::string_view HEADER_NAME_X_CACHE          = "x-cache";
-constexpr std::string_view HEADER_NAME_X_GENERATION     = "x-cache-generation";
-constexpr std::string_view HEADER_NAME_X_TRANSACTION_ID = "x-transaction-id";
-constexpr std::string_view HEADER_NAME_X_DUMP_HEADERS   = "x-dump-headers";
-constexpr std::string_view HEADER_NAME_X_REMAP          = "x-remap";
-constexpr std::string_view HEADER_NAME_X_PROBE_HEADERS  = "probe";
-constexpr std::string_view HEADER_NAME_X_PSELECT_KEY    = 
"x-parentselection-key";
-constexpr std::string_view HEADER_NAME_X_CACHE_INFO     = "x-cache-info";
-constexpr std::string_view HEADER_NAME_X_EFFECTIVE_URL  = "x-effective-url";
-constexpr std::string_view HEADER_NAME_VIA              = "via";
-constexpr std::string_view HEADER_NAME_DIAGS            = "diags";
-constexpr std::string_view HEADER_NAME_ALL              = "all";
+constexpr std::string_view HEADER_NAME_X_CACHE_KEY       = "x-cache-key";
+constexpr std::string_view HEADER_NAME_X_MILESTONES      = "x-milestones";
+constexpr std::string_view HEADER_NAME_X_CACHE           = "x-cache";
+constexpr std::string_view HEADER_NAME_X_GENERATION      = 
"x-cache-generation";
+constexpr std::string_view HEADER_NAME_X_TRANSACTION_ID  = "x-transaction-id";
+constexpr std::string_view HEADER_NAME_X_DUMP_HEADERS    = "x-dump-headers";
+constexpr std::string_view HEADER_NAME_X_REMAP           = "x-remap";
+constexpr std::string_view HEADER_NAME_X_PROBE_HEADERS   = "probe";
+constexpr std::string_view HEADER_NAME_X_PSELECT_KEY     = 
"x-parentselection-key";
+constexpr std::string_view HEADER_NAME_X_CACHE_INFO      = "x-cache-info";
+constexpr std::string_view HEADER_NAME_X_EFFECTIVE_URL   = "x-effective-url";
+constexpr std::string_view HEADER_NAME_VIA               = "via";
+constexpr std::string_view HEADER_NAME_DIAGS             = "diags";
+constexpr std::string_view HEADER_NAME_X_PROBE_FULL_JSON = "probe-full-json";
+constexpr std::string_view HEADER_NAME_ALL               = "all";
 
 constexpr struct XHeader {
   std::string_view name;
   unsigned int     flag;
 } header_flags[] = {
-  {HEADER_NAME_X_CACHE_KEY,      XHEADER_X_CACHE_KEY     },
-  {HEADER_NAME_X_MILESTONES,     XHEADER_X_MILESTONES    },
-  {HEADER_NAME_X_CACHE,          XHEADER_X_CACHE         },
-  {HEADER_NAME_X_GENERATION,     XHEADER_X_GENERATION    },
-  {HEADER_NAME_X_TRANSACTION_ID, XHEADER_X_TRANSACTION_ID},
-  {HEADER_NAME_X_DUMP_HEADERS,   XHEADER_X_DUMP_HEADERS  },
-  {HEADER_NAME_X_REMAP,          XHEADER_X_REMAP         },
-  {HEADER_NAME_X_PROBE_HEADERS,  XHEADER_X_PROBE_HEADERS },
-  {HEADER_NAME_X_PSELECT_KEY,    XHEADER_X_PSELECT_KEY   },
-  {HEADER_NAME_X_CACHE_INFO,     XHEADER_X_CACHE_INFO    },
-  {HEADER_NAME_X_EFFECTIVE_URL,  XHEADER_X_EFFECTIVE_URL },
-  {HEADER_NAME_VIA,              XHEADER_VIA             },
-  {HEADER_NAME_DIAGS,            XHEADER_DIAGS           },
-  {HEADER_NAME_ALL,              XHEADER_ALL             },
+  {HEADER_NAME_X_CACHE_KEY,       XHEADER_X_CACHE_KEY      },
+  {HEADER_NAME_X_MILESTONES,      XHEADER_X_MILESTONES     },
+  {HEADER_NAME_X_CACHE,           XHEADER_X_CACHE          },
+  {HEADER_NAME_X_GENERATION,      XHEADER_X_GENERATION     },
+  {HEADER_NAME_X_TRANSACTION_ID,  XHEADER_X_TRANSACTION_ID },
+  {HEADER_NAME_X_DUMP_HEADERS,    XHEADER_X_DUMP_HEADERS   },
+  {HEADER_NAME_X_REMAP,           XHEADER_X_REMAP          },
+  {HEADER_NAME_X_PROBE_HEADERS,   XHEADER_X_PROBE_HEADERS  },
+  {HEADER_NAME_X_PSELECT_KEY,     XHEADER_X_PSELECT_KEY    },
+  {HEADER_NAME_X_CACHE_INFO,      XHEADER_X_CACHE_INFO     },
+  {HEADER_NAME_X_EFFECTIVE_URL,   XHEADER_X_EFFECTIVE_URL  },
+  {HEADER_NAME_VIA,               XHEADER_VIA              },
+  {HEADER_NAME_DIAGS,             XHEADER_DIAGS            },
+  {HEADER_NAME_X_PROBE_FULL_JSON, XHEADER_X_PROBE_FULL_JSON},
+  {HEADER_NAME_ALL,               XHEADER_ALL              },
 };
 
 static TSCont XInjectHeadersCont  = nullptr;
@@ -447,7 +433,7 @@ InjectEffectiveURLHeader(TSHttpTxn txn, TSMBuffer buffer, 
TSMLoc hdr)
 }
 
 static void
-InjectOriginalContentTypeHeader(TSMBuffer buffer, TSMLoc hdr)
+InjectOriginalContentTypeHeader(TSMBuffer buffer, TSMLoc hdr, bool 
is_full_json)
 {
   TSMLoc ct_field = TSMimeHdrFieldFind(buffer, hdr, 
TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE);
   if (TS_NULL_MLOC != ct_field) {
@@ -466,7 +452,12 @@ InjectOriginalContentTypeHeader(TSMBuffer buffer, TSMLoc 
hdr)
   }
 
   TSMimeHdrFieldValuesClear(buffer, hdr, ct_field);
-  TSReleaseAssert(TSMimeHdrFieldValueStringSet(buffer, hdr, ct_field, -1, 
"text/plain", lengthof("text/plain")) == TS_SUCCESS);
+  if (is_full_json) {
+    TSReleaseAssert(TSMimeHdrFieldValueStringSet(buffer, hdr, ct_field, -1, 
"application/json", lengthof("application/json")) ==
+                    TS_SUCCESS);
+  } else {
+    TSReleaseAssert(TSMimeHdrFieldValueStringSet(buffer, hdr, ct_field, -1, 
"text/plain", lengthof("text/plain")) == TS_SUCCESS);
+  }
 }
 
 static void
@@ -587,10 +578,11 @@ XInjectResponseHeaders(TSCont /* contp */, TSEvent event, 
void *edata)
     log_headers(txn, buffer, hdr, "ClientResponse");
   }
 
-  if (xheaders & XHEADER_X_PROBE_HEADERS) {
-    InjectOriginalContentTypeHeader(buffer, hdr);
+  if (xheaders & XHEADER_X_PROBE_HEADERS || xheaders & 
XHEADER_X_PROBE_FULL_JSON) {
+    InjectOriginalContentTypeHeader(buffer, hdr, xheaders & 
XHEADER_X_PROBE_FULL_JSON);
     BodyBuilder *data = AuxDataMgr::data(txn).body_builder.get();
-    Dbg(dbg_ctl_xform, "XInjectResponseHeaders(): client resp header ready");
+    Dbg(dbg_ctl_xform, "XInjectResponseHeaders(): client resp header ready 
(probe-full-json: %d)",
+        xheaders & XHEADER_X_PROBE_FULL_JSON);
     if (data == nullptr) {
       TSHttpTxnReenable(txn, TS_EVENT_HTTP_ERROR);
       return TS_ERROR;
@@ -696,6 +688,12 @@ XScanRequestHeaders(TSCont /* contp */, TSEvent event, 
void *edata)
 
 #define header_field_eq(name, vptr, vlen) (((int)name.size() == vlen) && 
(strncasecmp(name.data(), vptr, vlen) == 0))
 
+      // A couple convenience variables for probing.
+      bool const is_probe_headers =
+        header_field_eq(HEADER_NAME_X_PROBE_HEADERS, value, vsize) && 
(XHEADER_X_PROBE_HEADERS & allowedHeaders);
+      bool const is_probe_full_json =
+        header_field_eq(HEADER_NAME_X_PROBE_FULL_JSON, value, vsize) && 
(XHEADER_X_PROBE_FULL_JSON & allowedHeaders);
+
       if (header_field_eq(HEADER_NAME_X_CACHE_KEY, value, vsize)) {
         xheaders |= XHEADER_X_CACHE_KEY & allowedHeaders;
       } else if (header_field_eq(HEADER_NAME_X_CACHE_INFO, value, vsize)) {
@@ -717,13 +715,18 @@ XScanRequestHeaders(TSCont /* contp */, TSEvent event, 
void *edata)
         // Enable diagnostics for DebugTxn()'s only
         TSHttpTxnCntlSet(txn, TS_HTTP_CNTL_TXN_DEBUG, true);
 
-      } else if (header_field_eq(HEADER_NAME_X_PROBE_HEADERS, value, vsize) && 
(XHEADER_X_PROBE_HEADERS & allowedHeaders)) {
-        xheaders |= XHEADER_X_PROBE_HEADERS;
+      } else if (is_probe_headers || is_probe_full_json) {
+        // You can't do both at the same time. If both are passed, full JSON 
takes precedence.
+        if (is_probe_headers && is_probe_full_json) {
+          TSError("[xdebug] Both probe-headers and probe-full-json are 
enabled. Choosing probe-full-json.");
+        }
+        xheaders |= is_probe_full_json ? XHEADER_X_PROBE_FULL_JSON : 
XHEADER_X_PROBE_HEADERS;
 
         auto &auxData = AuxDataMgr::data(txn);
 
         // prefix request headers and postfix response headers
         BodyBuilder *data = new BodyBuilder();
+        data->probe_type  = is_probe_full_json ? ProbeType::PROBE_FULL_JSON : 
ProbeType::PROBE_STANDARD;
         auxData.body_builder.reset(data);
 
         TSVConn connp = TSTransformCreate(body_transform, txn);
@@ -839,11 +842,18 @@ updateAllowedHeaders(const char *optarg)
   TSfree(list);
 }
 
+} // namespace xdebug
+
 void
 TSPluginInit(int argc, const char *argv[])
 {
+  using namespace xdebug;
+
   Dbg(dbg_ctl, "initializing plugin");
 
+  // Initialize transforms module
+  init_transforms();
+
   static const struct option longopt[] = {
     {const_cast<char *>("header"), required_argument, nullptr, 'h' },
     {const_cast<char *>("enable"), required_argument, nullptr, 'e' },
@@ -903,6 +913,4 @@ TSPluginInit(int argc, const char *argv[])
   XDeleteDebugHdrCont = TSContCreate(XDeleteDebugHdr, nullptr);
   TSReleaseAssert(XDeleteDebugHdrCont);
   TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, 
TSContCreate(XScanRequestHeaders, nullptr));
-
-  gethostname(Hostname, 1024);
 }
diff --git a/plugins/xdebug/xdebug_headers.cc b/plugins/xdebug/xdebug_headers.cc
index f7edbd8ba4..c383b0f6c7 100644
--- a/plugins/xdebug/xdebug_headers.cc
+++ b/plugins/xdebug/xdebug_headers.cc
@@ -17,8 +17,10 @@
   limitations under the License.
  */
 
+#include "xdebug_headers.h"
+
 #include <cstdlib>
-#include <stdio.h>
+#include <cstdio>
 #include <cstdio>
 #include <strings.h>
 #include <string_view>
@@ -28,14 +30,19 @@
 
 #define DEBUG_TAG_LOG_HEADERS "xdebug.headers"
 
+namespace xdebug
+{
+
 namespace
 {
-DbgCtl dbg_ctl_hdrs{DEBUG_TAG_LOG_HEADERS};
+  DbgCtl dbg_ctl_hdrs{DEBUG_TAG_LOG_HEADERS};
 }
 
 class EscapeCharForJson
 {
 public:
+  EscapeCharForJson(bool full_json) : _full_json(full_json) {}
+
   std::string_view
   operator()(char const &c)
   {
@@ -44,19 +51,23 @@ public:
     }
     if ((IN_NAME == _state) && (':' == c)) {
       _state = BEFORE_VALUE;
-      return {"' : '"};
+      if (_full_json) {
+        return {R"(":")"};
+      } else {
+        return {"' : '"};
+      }
     }
     if ('\r' == c) {
       return {""};
     }
     if ('\n' == c) {
-      std::string_view result{_after_value()};
+      std::string_view result{_after_value(_full_json)};
 
       if (BEFORE_NAME == _state) {
         return {""};
       } else if (BEFORE_VALUE == _state) {
         // Failsafe -- missing value -- this should never happen.
-        result = _missing_value();
+        result = _missing_value(_full_json);
       }
       _state = BEFORE_NAME;
       return result;
@@ -87,50 +98,70 @@ public:
   // After last header line, back up and throw away everything but the closing 
quote.
   //
   static std::size_t
-  backup()
+  backup(bool full_json)
   {
-    return _after_value().size() - 1;
+    return _after_value(full_json).size() - 1;
   }
 
 private:
+  /** The separator content between fields. */
   static std::string_view
-  _missing_value()
+  _after_value(bool full_json)
   {
-    return {"' : '',\n\t'"};
-  }
-
-  static std::string_view
-  _after_name()
-  {
-    return {_missing_value().data(), 5};
+    if (full_json) {
+      return {R"(",")"};
+    } else {
+      return {"',\n\t'"};
+    }
   }
 
+  /** The separator content when there is an empty value.
+   *
+   * This is hopefully never used.
+   */
   static std::string_view
-  _after_value()
+  _missing_value(bool full_json)
   {
-    return {_missing_value().data() + 5, 5};
+    if (full_json) {
+      return {R"(":")"};
+    } else {
+      return {"' : '','\n\t"};
+    }
   }
 
+private:
   enum _State { BEFORE_NAME, IN_NAME, BEFORE_VALUE, IN_VALUE };
 
+  /// Initial state is BEFORE_VALUE to parse the start line.
   _State _state{BEFORE_VALUE};
+
+  /** Whether to print the headers for x-probe-full-json.
+   *
+   * The legacy "probe" format is not JSON-compliant. The new "probe-full-json"
+   * format is JSON-compliant.
+   */
+  bool _full_json = false;
 };
 
 ///////////////////////////////////////////////////////////////////////////
 // Dump a header on stderr, useful together with Dbg().
 void
-print_headers(TSMBuffer bufp, TSMLoc hdr_loc, std::stringstream &ss)
+print_headers(TSMBuffer bufp, TSMLoc hdr_loc, std::stringstream &ss, bool 
full_json)
 {
   TSIOBuffer        output_buffer;
   TSIOBufferReader  reader;
   TSIOBufferBlock   block;
   const char       *block_start;
   int64_t           block_avail;
-  EscapeCharForJson escape_char_for_json;
+  EscapeCharForJson escape_char_for_json{full_json};
   output_buffer = TSIOBufferCreate();
   reader        = TSIOBufferReaderAlloc(output_buffer);
 
-  ss << "\t'Start-Line' : '";
+  if (full_json) {
+    ss << R"("start-line":")";
+  } else {
+    ss << "\t'Start-Line' : '";
+  }
 
   // Print all message header lines.
   TSHttpHdrPrint(bufp, hdr_loc, output_buffer);
@@ -146,7 +177,7 @@ print_headers(TSMBuffer bufp, TSMLoc hdr_loc, 
std::stringstream &ss)
     block = TSIOBufferReaderStart(reader);
   } while (block && block_avail != 0);
 
-  ss.seekp(-escape_char_for_json.backup(), std::ios_base::end);
+  ss.seekp(-escape_char_for_json.backup(full_json), std::ios_base::end);
 
   /* Free up the TSIOBuffer that we used to print out the header */
   TSIOBufferReaderFree(reader);
@@ -160,7 +191,7 @@ log_headers(TSHttpTxn /* txn ATS_UNUSED */, TSMBuffer bufp, 
TSMLoc hdr_loc, char
 {
   if (dbg_ctl_hdrs.on()) {
     std::stringstream output;
-    print_headers(bufp, hdr_loc, output);
+    print_headers(bufp, hdr_loc, output, FULL_JSON);
     Dbg(dbg_ctl_hdrs, "\n=============\n %s headers are... \n %s", type_msg, 
output.str().c_str());
   }
 }
@@ -172,13 +203,13 @@ print_request_headers(TSHttpTxn txn, std::stringstream 
&output)
   TSMLoc    hdr_loc;
   if (TSHttpTxnClientReqGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
     output << "{'type':'request', 'side':'client', 'headers': {\n";
-    print_headers(buf_c, hdr_loc, output);
+    print_headers(buf_c, hdr_loc, output, !FULL_JSON);
     output << "\n\t}}";
     TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
   }
   if (TSHttpTxnServerReqGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
     output << ",{'type':'request', 'side':'server', 'headers': {\n";
-    print_headers(buf_s, hdr_loc, output);
+    print_headers(buf_s, hdr_loc, output, !FULL_JSON);
     output << "\n\t}}";
     TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
   }
@@ -191,14 +222,70 @@ print_response_headers(TSHttpTxn txn, std::stringstream 
&output)
   TSMLoc    hdr_loc;
   if (TSHttpTxnServerRespGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
     output << "{'type':'response', 'side':'server', 'headers': {\n";
-    print_headers(buf_s, hdr_loc, output);
+    print_headers(buf_s, hdr_loc, output, !FULL_JSON);
     output << "\n\t}},";
     TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
   }
   if (TSHttpTxnClientRespGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
     output << "{'type':'response', 'side':'client', 'headers': {\n";
-    print_headers(buf_c, hdr_loc, output);
+    print_headers(buf_c, hdr_loc, output, !FULL_JSON);
     output << "\n\t}}";
     TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
   }
 }
+
+void
+print_request_headers_full_json(TSHttpTxn txn, std::stringstream &output)
+{
+  TSMBuffer buf_c, buf_s;
+  TSMLoc    hdr_loc;
+
+  bool has_client = false;
+
+  Dbg(dbg_ctl_hdrs, "Printing client request headers for full JSON");
+  if (TSHttpTxnClientReqGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
+    output << "{\"client-request\":{";
+    print_headers(buf_c, hdr_loc, output, FULL_JSON);
+    output << "}";
+    has_client = true;
+    TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
+  }
+
+  if (TSHttpTxnServerReqGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
+    if (has_client) {
+      output << ",";
+    }
+    output << "\"proxy-request\":{";
+    print_headers(buf_s, hdr_loc, output, FULL_JSON);
+    output << "}";
+    TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
+  }
+}
+
+void
+print_response_headers_full_json(TSHttpTxn txn, std::stringstream &output)
+{
+  TSMBuffer buf_c, buf_s;
+  TSMLoc    hdr_loc;
+
+  bool has_server = false;
+
+  if (TSHttpTxnServerRespGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
+    output << "\"server-response\":{";
+    print_headers(buf_s, hdr_loc, output, FULL_JSON);
+    output << "}";
+    has_server = true;
+    TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
+  }
+
+  if (TSHttpTxnClientRespGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
+    if (has_server) {
+      output << ",";
+    }
+    output << "\"proxy-response\":{";
+    print_headers(buf_c, hdr_loc, output, FULL_JSON);
+    output << "}}";
+    TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
+  }
+}
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_headers.h b/plugins/xdebug/xdebug_headers.h
new file mode 100644
index 0000000000..0276b5ff4b
--- /dev/null
+++ b/plugins/xdebug/xdebug_headers.h
@@ -0,0 +1,83 @@
+/** @file
+ *
+ * XDebug plugin headers functionality declarations.
+ *
+ *  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.
+ */
+
+#pragma once
+
+#include <sstream>
+#include "ts/ts.h"
+
+namespace xdebug
+{
+
+/**
+ * Whether to print the headers for the "probe-full-json" format.
+ */
+static constexpr bool FULL_JSON = true;
+
+/**
+ * Print headers to a stringstream with JSON-like formatting.
+ * @param bufp The TSMBuffer containing the headers.
+ * @param hdr_loc The TSMLoc for the headers.
+ * @param ss The stringstream to write to.
+ * @param full_json Whether to print the headers in a compliant JSON
+ * format. The legacy "probe" format is not JSON-compliant. The new
+ * "probe-full-json" format is JSON-compliant.
+ */
+void print_headers(TSMBuffer bufp, TSMLoc hdr_loc, std::stringstream &ss, bool 
full_json);
+
+/**
+ * Log headers to debug for debugging purposes.
+ * @param txn The transaction.
+ * @param bufp The TSMBuffer containing the headers.
+ * @param hdr_loc The TSMLoc for the headers.
+ * @param type_msg Description of the header type.
+ */
+void log_headers(TSHttpTxn txn, TSMBuffer bufp, TSMLoc hdr_loc, char const 
*type_msg);
+
+/**
+ * Print request headers in the "probe" format.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_request_headers(TSHttpTxn txn, std::stringstream &output);
+
+/**
+ * Print response headers in the "probe" format.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_response_headers(TSHttpTxn txn, std::stringstream &output);
+
+/**
+ * Print request headers in JSON format for probe-full-json.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_request_headers_full_json(TSHttpTxn txn, std::stringstream &output);
+
+/**
+ * Print response headers in JSON format for probe-full-json.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_response_headers_full_json(TSHttpTxn txn, std::stringstream 
&output);
+
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_transforms.cc 
b/plugins/xdebug/xdebug_transforms.cc
index 3c4fe1c79f..8369e30b30 100644
--- a/plugins/xdebug/xdebug_transforms.cc
+++ b/plugins/xdebug/xdebug_transforms.cc
@@ -17,19 +17,30 @@
   limitations under the License.
  */
 
-#include <limits.h>
-#include <stdio.h>
-#include <string.h>
-#include <functional>
+#include "xdebug_types.h"
+#include "xdebug_headers.h"
+
+#include <unistd.h>
+#include <sstream>
+#include <cinttypes>
 
 #include "ts/ts.h"
 
+namespace xdebug
+{
+
 static const std::string_view MultipartBoundary{"\r\n--- ATS xDebug Probe 
Injection Boundary ---\r\n\r\n"};
 
 static char Hostname[1024];
 
 static DbgCtl dbg_ctl_xform{"xdebug_transform"};
 
+void
+init_transforms()
+{
+  gethostname(Hostname, 1024);
+}
+
 static std::string
 getPreBody(TSHttpTxn txn)
 {
@@ -41,6 +52,15 @@ getPreBody(TSHttpTxn txn)
   return output.str();
 }
 
+static std::string
+getPreBodyFullJson(TSHttpTxn txn)
+{
+  std::stringstream output;
+  print_request_headers_full_json(txn, output);
+  output << R"(,"server-body": ")";
+  return output.str();
+}
+
 static std::string
 getPostBody(TSHttpTxn txn)
 {
@@ -52,12 +72,27 @@ getPostBody(TSHttpTxn txn)
   return output.str();
 }
 
-static void
+static std::string
+getPostBodyFullJson(TSHttpTxn txn)
+{
+  std::stringstream output;
+  output << R"(",)"; // Close the origin-body field.
+  print_response_headers_full_json(txn, output);
+  output << '\n';
+  return output.str();
+}
+
+void
 writePostBody(TSHttpTxn txn, BodyBuilder *data)
 {
   if (data->wrote_body && data->hdr_ready && 
!data->wrote_postbody.test_and_set()) {
     Dbg(dbg_ctl_xform, "body_transform(): Writing postbody headers...");
-    std::string postbody = getPostBody(txn);
+    std::string postbody;
+    if (data->probe_type == ProbeType::PROBE_STANDARD) {
+      postbody = getPostBody(txn);
+    } else {
+      postbody = getPostBodyFullJson(txn);
+    }
     TSIOBufferWrite(data->output_buffer.get(), postbody.data(), 
postbody.length());
     data->nbytes += postbody.length();
     TSVIONBytesSet(data->output_vio, data->nbytes);
@@ -65,7 +100,7 @@ writePostBody(TSHttpTxn txn, BodyBuilder *data)
   }
 }
 
-static int
+int
 body_transform(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED */)
 {
   TSHttpTxn    txn  = static_cast<TSHttpTxn>(TSContDataGet(contp));
@@ -102,7 +137,12 @@ body_transform(TSCont contp, TSEvent event, void * /* 
edata ATS_UNUSED */)
 
     if (data->wrote_prebody == false) {
       Dbg(dbg_ctl_xform, "body_transform(): Writing prebody headers...");
-      std::string prebody = getPreBody(txn);
+      std::string prebody;
+      if (data->probe_type == ProbeType::PROBE_STANDARD) {
+        prebody = getPreBody(txn);
+      } else {
+        prebody = getPreBodyFullJson(txn);
+      }
       TSIOBufferWrite(data->output_buffer.get(), prebody.data(), 
prebody.length()); // write prebody
       data->wrote_prebody  = true;
       data->nbytes        += prebody.length();
@@ -142,3 +182,5 @@ body_transform(TSCont contp, TSEvent event, void * /* edata 
ATS_UNUSED */)
   }
   return 0;
 }
+
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_transforms.h 
b/plugins/xdebug/xdebug_transforms.h
new file mode 100644
index 0000000000..f1ce35b8b8
--- /dev/null
+++ b/plugins/xdebug/xdebug_transforms.h
@@ -0,0 +1,51 @@
+/** @file
+ *
+ * XDebug plugin transforms functionality declarations.
+ *
+ *  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.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+#include "xdebug_types.h" // Required for BodyBuilder
+
+namespace xdebug
+{
+
+/**
+ * Initialize the hostname for the transforms module.
+ */
+void init_transforms();
+
+/**
+ * Write the post body data (called after body is complete).
+ * @param txn The transaction.
+ * @param data The BodyBuilder data.
+ */
+void writePostBody(TSHttpTxn txn, BodyBuilder *data);
+
+/**
+ * Main body transformation continuation handler.
+ * @param contp The continuation.
+ * @param event The event type.
+ * @param edata Event data (unused).
+ * @return Status code.
+ */
+int body_transform(TSCont contp, TSEvent event, void *edata);
+
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_types.h b/plugins/xdebug/xdebug_types.h
new file mode 100644
index 0000000000..d6f696feea
--- /dev/null
+++ b/plugins/xdebug/xdebug_types.h
@@ -0,0 +1,58 @@
+/** @file
+ *
+ * XDebug plugin common types and definitions.
+ *
+ *  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.
+ */
+
+#pragma once
+
+#include <atomic>
+#include <memory>
+#include <string>
+#include "ts/ts.h"
+#include "tscpp/api/Cleanup.h"
+
+namespace xdebug
+{
+
+enum class ProbeType { PROBE_STANDARD, PROBE_FULL_JSON };
+
+struct BodyBuilder {
+  atscppapi::TSContUniqPtr     transform_connp;
+  atscppapi::TSIOBufferUniqPtr output_buffer;
+  // It's important that output_reader comes after output_buffer so it will be 
deleted first.
+  atscppapi::TSIOBufferReaderUniqPtr output_reader;
+  TSVIO                              output_vio    = nullptr;
+  bool                               wrote_prebody = false;
+  bool                               wrote_body    = false;
+  bool                               hdr_ready     = false;
+  std::atomic_flag                   wrote_postbody;
+  ProbeType                          probe_type = ProbeType::PROBE_STANDARD;
+
+  int64_t nbytes = 0;
+};
+
+struct XDebugTxnAuxData {
+  std::unique_ptr<BodyBuilder> body_builder;
+  unsigned                     xheaders = 0;
+};
+
+extern atscppapi::TxnAuxMgrData mgrData;
+using AuxDataMgr = atscppapi::TxnAuxDataMgr<XDebugTxnAuxData, mgrData>;
+
+} // namespace xdebug
diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml 
b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml
new file mode 100644
index 0000000000..4dd69b515e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml
@@ -0,0 +1,58 @@
+#  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.
+
+sessions:
+
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /test
+      version: '1.1'
+      headers:
+        fields:
+        - [ uuid, '1' ]
+        - [ Host, example.com ]
+        - [ X-Debug, probe ]
+        - [ User-Agent, "XDebug-Test/1.0" ]
+
+    proxy-request:
+      headers:
+        fields:
+        - [ X-Debug, { as: absent } ]
+        - [ User-Agent, { value: "XDebug-Test/1.0", as: equal } ]
+
+    server-response:
+      status: 200
+      headers:
+        fields:
+        - [ Content-Type, "text/html" ]
+        - [ Content-Length, "20" ]
+        - [ X-Server, "TestOrigin" ]
+      content:
+        encoding: plain
+        data: "Original server body"
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ Content-Type, { value: "text/plain", as: equal } ]
+        - [ X-Original-Content-Type, { value: "text/html", as: equal } ]
+        - [ X-Server, { value: "TestOrigin", as: contains } ]
+      content:
+        data: "Original server body"
+        verify: { as: contains }
diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py 
b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py
new file mode 100644
index 0000000000..303223ea8e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py
@@ -0,0 +1,80 @@
+'''
+Verify xdebug plugin probe functionality.
+'''
+#  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.
+
+Test.Summary = 'Test xdebug plugin probe functionality'
+Test.ContinueOnFail = True
+Test.SkipUnless(Condition.PluginExists('xdebug.so'))
+
+
+class XDebugProbeTest:
+    """
+    Test the xdebug probe functionality which transforms the response body
+    to include request and response headers in a multipart format.
+
+    The probe feature:
+    - Changes Content-Type to text/plain
+    - Injects request headers before the original body
+    - Injects response headers after the original body
+    - Uses multipart boundary separators
+    - Disables caching due to body modification
+    """
+
+    _replay_file: str = "x_probe.replay.yaml"
+
+    def __init__(self) -> None:
+        tr = Test.AddTestRun("xdebug probe test")
+        self._setupOriginServer(tr)
+        self._setupTS(tr)
+        self._setupClient(tr)
+
+    def _setupOriginServer(self, tr: 'TestRun') -> None:
+        """Configure the origin server using Proxy Verifier.
+        :param tr: TestRun to add the server to.
+        """
+        self._server = tr.AddVerifierServerProcess("server", self._replay_file)
+
+    def _setupTS(self, tr: 'TestRun') -> None:
+        """Configure ATS with xdebug plugin enabled for probe functionality.
+        :param tr: TestRun to add the ATS process to.
+        """
+        self._ts = Test.MakeATSProcess("ts")
+
+        self._ts.Disk.records_config.update({
+            "proxy.config.diags.debug.enabled": 1,
+            "proxy.config.diags.debug.tags": "xdebug",
+        })
+
+        self._ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe')
+        self._ts.Disk.remap_config.AddLine(f"map / 
http://127.0.0.1:{self._server.Variables.http_port}";)
+
+    def _setupClient(self, tr: 'TestRun') -> None:
+        """Test basic probe functionality with header injection.
+        :param tr: TestRun to add the test to.
+        """
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+        tr.AddVerifierClientProcess("client", self._replay_file, 
http_ports=[self._ts.Variables.port])
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+            'ATS xDebug Probe Injection Boundary', "ATS xDebug Probe Injection 
Boundary should be present")
+        tr.Processes.Default.Streams.stdout += 
Testers.ContainsExpression('xDebugProbeAt', "xDebugProbeAt should be present")
+
+
+# Execute the test
+XDebugProbeTest()
diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq.gold 
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq.gold
new file mode 100644
index 0000000000..27fb0a3b1b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq.gold
@@ -0,0 +1,3 @@
+"1"
+"Original server response"
+"from-origin"
diff --git 
a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml
 
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml
new file mode 100644
index 0000000000..7012f9c280
--- /dev/null
+++ 
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml
@@ -0,0 +1,57 @@
+#  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.
+
+sessions:
+
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /test
+      version: '1.1'
+      headers:
+        fields:
+        - [ uuid, '1' ]
+        - [ Host, example.com ]
+        - [ X-Debug, probe-full-json ]
+        - [ X-Request, "from-client"]
+
+    proxy-request:
+      headers:
+        fields:
+        - [ x-debug, { as: absent } ]
+
+    server-response:
+      status: 200
+      headers:
+        fields:
+        - [ Content-Type, "text/html" ]
+        - [ Content-Length, "24" ]
+        - [ X-Response, "from-origin" ]
+      content:
+        encoding: plain
+        data: "Original server response"
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ Content-Type, { value: "application/json", as: equal } ]
+        - [ X-Response, { value: "from-origin", as: contains } ]
+        - [ X-Original-Content-Type, { value: "text/html", as: equal } ]
+      content:
+        data: "Original server response"
+        verify: { as: contains }
diff --git 
a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py
 
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py
new file mode 100644
index 0000000000..4f92e423f3
--- /dev/null
+++ 
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py
@@ -0,0 +1,98 @@
+'''
+Verify xdebug plugin probe-full-json functionality.
+'''
+#  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.
+
+Test.Summary = 'Test xdebug plugin probe-full-json functionality'
+Test.ContinueOnFail = True
+Test.SkipUnless(Condition.PluginExists('xdebug.so'))
+Test.SkipUnless(Condition.HasProgram("jq", "jq is required to validate JSON 
output"))
+
+
+class XDebugProbeFullJsonTest:
+    """
+    Test the xdebug probe-full-json functionality which transforms the 
response body
+    to include request headers, response body, and response headers in a 
complete JSON format.
+
+    The probe-full-json feature:
+    - Changes Content-Type to text/plain
+    - Generates a complete JSON object containing all debug information
+    - Includes client/server request headers, response body, and client/server 
response headers
+    - Disables caching due to body modification
+    """
+
+    _replay_file: str = "x_probe_full_json.replay.yaml"
+
+    def __init__(self) -> None:
+        self._servers_are_started: bool = False
+        self._setupOriginServer()
+        self._setupTS()
+        self._setupClient()
+        self._setupJqValidation()
+
+    def _setupOriginServer(self) -> None:
+        """Configure the origin server using Proxy Verifier.
+        """
+        self._server = Test.MakeVerifierServerProcess("server", 
self._replay_file)
+
+    def _setupTS(self) -> None:
+        """Configure ATS with xdebug plugin enabled for probe-full-json 
functionality.
+        """
+        self._ts = Test.MakeATSProcess("ts")
+
+        self._ts.Disk.records_config.update({
+            "proxy.config.diags.debug.enabled": 1,
+            "proxy.config.diags.debug.tags": "xdebug",
+        })
+
+        self._ts.Disk.plugin_config.AddLine('xdebug.so 
--enable=probe-full-json')
+        self._ts.Disk.remap_config.AddLine(f"map / 
http://127.0.0.1:{self._server.Variables.http_port}";)
+
+    def _startServersIfNeeded(self, tr: 'TestRun') -> None:
+        '''Start the servers if they are not already started.
+        :param tr: TestRun to add the test to.
+        '''
+        if not self._servers_are_started:
+            tr.Processes.Default.StartBefore(self._server)
+            tr.Processes.Default.StartBefore(self._ts)
+            self._servers_are_started = True
+
+    def _setupClient(self) -> None:
+        """Test basic probe-full-json functionality with JSON output.
+        """
+        tr = Test.AddTestRun("Verify probe-full-json functionality")
+        self._startServersIfNeeded(tr)
+        tr.AddVerifierClientProcess("client", self._replay_file, 
http_ports=[self._ts.Variables.port])
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+            'X-Original-Content-Type', "X-Original-Content-Type should be 
present")
+
+    def _setupJqValidation(self) -> None:
+        """Use curl to get the response body and pipe through jq to validate 
JSON.
+        """
+        tr = Test.AddTestRun("Verify JSON output")
+        self._startServersIfNeeded(tr)
+        tr.MakeCurlCommand(
+            f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: 
probe-full-json" '
+            f'http://127.0.0.1:{self._ts.Variables.port}/test | '
+            "jq 
'.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'")
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stdout = "gold/jq.gold"
+
+
+# Execute the test
+XDebugProbeFullJsonTest()


Reply via email to