-
Notifications
You must be signed in to change notification settings - Fork 1
Content Formatting
Diagram notes can contain a lot of raw HTTP data — headers, JSON bodies, tokens, cookies, HTML responses. Without processing, this often results in diagrams that are noisy, unreadable, or too large to render. The content formatting system lets you transform, redact, and reshape this content before it appears in diagrams.
There are two levels of control:
-
ReportConfigurationOptions.RequestResponsePostProcessor— A singleFunc<string, string>applied to both requests and responses after formatting. This is the simplest approach and sufficient for most projects. -
DiagramsFetcherOptions— Fine-grained control with separate pre- and post-processors for requests and responses independently. For advanced scenarios.
The simplest way to process diagram content is via RequestResponsePostProcessor on ReportConfigurationOptions. This single function is applied to both request and response content after the library has formatted it (JSON pretty-printed, headers laid out):
new ReportConfigurationOptions
{
SpecificationsTitle = "My API Specifications",
RequestResponsePostProcessor = content => content
.RedactBearerTokens()
.RedactAccessTokens()
.SplitLongWords()
};This is what most integration guides show. Internally, the library maps this to both RequestPostFormattingProcessor and ResponsePostFormattingProcessor on DiagramsFetcherOptions.
For fine-grained control over how request/response bodies and headers are formatted in diagram notes, use DiagramsFetcherOptions directly. This is useful when you need different processing for requests vs responses, or when you need to transform the raw body before the library formats it.
var options = new DiagramsFetcherOptions
{
PlantUmlServerBaseUrl = "https://plantuml.com/plantuml",
RequestPreFormattingProcessor = content => content,
RequestPostFormattingProcessor = content => content,
ResponsePreFormattingProcessor = content => content,
ResponsePostFormattingProcessor = content => content,
ExcludedHeaders = ["Authorization", "X-Api-Key"]
};| Property | Type | Default | Description |
|---|---|---|---|
PlantUmlServerBaseUrl |
string |
"https://plantuml.com/plantuml" |
Base URL of the PlantUML server. |
RequestPreFormattingProcessor |
Func<string, string>? |
null |
Transform raw request body before the library formats it into the PlantUML note. |
RequestPostFormattingProcessor |
Func<string, string>? |
null |
Transform the formatted request note after the library has formatted it. |
ResponsePreFormattingProcessor |
Func<string, string>? |
null |
Transform raw response body before the library formats it into the PlantUML note. |
ResponsePostFormattingProcessor |
Func<string, string>? |
null |
Transform the formatted response note after the library has formatted it. |
ExcludedHeaders |
IEnumerable<string> |
[] |
HTTP headers to exclude from diagram notes. |
SeparateSetup |
bool |
false |
When true, HTTP calls made before StartAction() are wrapped in a visual "Setup" partition in the diagram. See Diagram Customisation. |
HighlightSetup |
bool |
true |
When true (and SeparateSetup is enabled), the setup partition is rendered with a background colour. When false, the partition has no background colour. |
The formatting pipeline for each request/response is:
Raw body → PreFormattingProcessor → Library formatting (JSON pretty-print, header layout) → PostFormattingProcessor → Diagram note
- Pre-processor — Runs on the raw body text before the library formats it (JSON pretty-printing, header extraction, etc.). Use this to deserialise, decrypt, decompress, or restructure the raw content.
- Post-processor — Runs on the fully formatted note text after the library has laid it out. Use this to redact sensitive values, shorten long strings, strip noise, or wrap long lines. This is the one you'll use most often.
Bearer tokens are long, opaque strings that add significant noise to diagrams without providing useful information. A simple regex replacement shortens them:
RequestResponsePostProcessor = content =>
Regex.Replace(content, @"Bearer [A-Za-z0-9\-._~+/]+=*", "Bearer ***")Before:
[Authorization=Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJ...]
After:
[Authorization=Bearer ***]
Tokens also appear in JSON response bodies (access tokens, refresh tokens, ID tokens). Target them with regex that matches the JSON structure:
private static readonly Regex AccessTokenRegex = new("(?<=\"access_token\": \")[^\"]+(?=\")");
private static readonly Regex RefreshTokenRegex = new("(?<=\"refresh_token\": \")[^\"]+(?=\")");
private static readonly Regex IdTokenRegex = new("(?<=\"id_token\": \")[^\"]+(?=\")");
RequestResponsePostProcessor = content => content
.RedactMiddle(AccessTokenRegex)
.RedactMiddle(RefreshTokenRegex)
.RedactMiddle(IdTokenRegex)Where RedactMiddle keeps the start and end of the token visible for debugging while redacting the bulk:
private static string RedactMiddle(this string value, Regex regex) =>
regex.Replace(value, m =>
m.Value.Length > 50
? m.Value[..8] + "_REDACTED_" + m.Value[^18..]
: m.Value);Before:
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.Sfl..."After:
"access_token": "eyJhbGc_REDACTED_KxwRJSMeKKF2QT4"Cookie headers often contain long session tokens that bloat diagrams:
private static readonly Regex SetCookieRegex = new(@"(?<=\[Set-Cookie=)[^\]]+(?=])");
private static readonly Regex CookieRegex = new(@"(?<=\[Cookie=)[^\]]+(?=])");
RequestResponsePostProcessor = content => content
.RedactEnding(SetCookieRegex, 50)
.RedactEnding(CookieRegex, 50)Where RedactEnding keeps only the first N characters:
private static string RedactEnding(this string value, Regex regex, int length = 30) =>
regex.Replace(value, m =>
m.Value.Length > 200
? m.Value[..length] + "_RedactedEnding"
: m.Value);Tokens, Base64-encoded values, and other long unbroken strings can make diagram notes extremely wide, sometimes exceeding PlantUML's rendering limits. Break them across lines:
private static string SplitWordsOverMaxLength(this string value, int maxLength = 200)
{
var words = value.Split("\n")
.SelectMany(line => line.Trim().Split(' '))
.Where(w => !string.IsNullOrWhiteSpace(w));
foreach (var word in words.Where(w => w.Length > maxLength))
{
var chunks = word.Chunk(maxLength).Select(c => new string(c));
value = value.Replace(word, string.Join("\n", chunks));
}
return value;
}Some payloads contain long string values (e.g. serialised JSON inside a JSON field, or EventGrid event payloads) that should be wrapped at a reasonable line length:
private static readonly Regex EventGridValueRegex =
new("(?<=\"(?:request|response)\": \")[^\"]+");
private static string WrapLongValues(this string value, int maxLineLength = 90) =>
EventGridValueRegex.Replace(value, match =>
match.Value.Length <= maxLineLength
? match.Value
: string.Join("\n", match.Value.Chunk(maxLineLength).Select(c => new string(c))));Some downstream services (identity providers, OAuth consent pages, etc.) return full HTML pages in their responses. These can be thousands of characters and make diagrams unrenderable. Replace them above a size threshold:
private const int HtmlMaxCharactersBeforeRedacting = 3_000;
private static string RedactLargeHtmlResponses(this string value)
{
var startTag = "<html";
var endTag = "</html";
if (!value.Contains(startTag))
return value;
if (value.Length > HtmlMaxCharactersBeforeRedacting)
{
var before = value.Split(startTag)[0];
var afterEnd = value.Split(startTag)[1].Split(endTag)[1];
return before + startTag + ">...REDACTED..." + endTag + afterEnd.Trim();
}
return value;
}Rather than just redacting tokens, you can extract useful information from JWTs and annotate them in the diagram. For example, extracting the auth_level claim:
private static string ExposeAuthLevelsOfAccessTokens(this string value)
{
var regex = new Regex("(\"access_token\": \")(.*)(\",)");
return regex.Replace(value, match =>
{
try
{
var token = new JwtSecurityTokenHandler().ReadJwtToken(match.Groups[2].Value);
var authLevel = token.Claims.FirstOrDefault(x => x.Type == "auth_level");
return authLevel is null
? match.Value
: match.Value + $" /* [auth_level={authLevel.Value}] */";
}
catch
{
return match.Value; // Not a JWT, leave as-is
}
});
}Before:
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJhdXRoX2xldmVsIjoiMiIsInN1YiI6InVzZXIxIn0.sig...",After:
"access_token": "eyJhbGc_REDACTED_KxwRJSMeKKF2QT4", /* [auth_level=2] */This gives you the security-relevant metadata at a glance without the raw token noise.
If your SUT communicates with SOAP or XML-based services, the raw body may be a single line of XML. Use a pre-processor to format it before the library processes it:
RequestPreFormattingProcessor = body =>
{
try { return XDocument.Parse(body).ToString(); }
catch { return body; }
}In practice, you'll combine multiple techniques into a fluent processing pipeline. Here's a complete real-world example for an API that deals with authentication tokens, cookies, and identity provider responses:
public static class DiagramContentProcessor
{
// --- Token patterns ---
private static readonly Regex AccessTokenRegex =
new("(?<=\"access_token\": \")[^\"]+(?=\")");
private static readonly Regex BearerTokenRegex =
new(@"(?<=Bearer )([^\]]+)");
private static readonly Regex RefreshTokenRegex =
new("(?<=\"refresh_token\": \")[^\"]+(?=\")");
private static readonly Regex IdTokenRegex =
new("(?<=\"id_token\": \")[^\"]+(?=\")");
// --- Cookie patterns ---
private static readonly Regex SetCookieRegex =
new(@"(?<=\[Set-Cookie=)[^\]]+(?=])");
private static readonly Regex CookieRegex =
new(@"(?<=\[Cookie=)[^\]]+(?=])");
// --- Session / long value patterns ---
private static readonly Regex SessionDataRegex =
new("(?<=\"sessionData\": \")[^\"]+(?=\")");
/// <summary>
/// The post-processor function — wire this into ReportConfigurationOptions.
/// </summary>
public static Func<string, string> PostProcessor => content => content
.ExposeAuthLevels()
.RedactMiddle(AccessTokenRegex)
.RedactMiddle(BearerTokenRegex)
.RedactMiddle(RefreshTokenRegex)
.RedactMiddle(IdTokenRegex)
.RedactEnding(SetCookieRegex, 50)
.RedactEnding(CookieRegex, 50)
.RedactEnding(SessionDataRegex, 30)
.RedactLargeHtml()
.SplitLongWords();
// --- Redaction helpers (extension methods) ---
private static string RedactMiddle(this string value, Regex regex) =>
regex.Replace(value, m =>
m.Value.Length > 50
? m.Value[..8] + "_REDACTED_" + m.Value[^18..]
: m.Value);
private static string RedactEnding(this string value, Regex regex, int length) =>
regex.Replace(value, m =>
m.Value.Length > 200
? m.Value[..length] + "_RedactedEnding"
: m.Value);
private static string ExposeAuthLevels(this string value)
{
var regex = new Regex("(\"access_token\": \")(.*)(\",)");
return regex.Replace(value, match =>
{
try
{
var token = new JwtSecurityTokenHandler().ReadJwtToken(match.Groups[2].Value);
var claim = token.Claims.FirstOrDefault(c => c.Type == "auth_level");
return claim is null ? match.Value : match.Value + $" /* [auth_level={claim.Value}] */";
}
catch { return match.Value; }
});
}
private static string RedactLargeHtml(this string value)
{
if (!value.Contains("<html")) return value;
if (value.Length <= 3_000) return value;
var before = value.Split("<html")[0];
var afterEnd = value.Split("<html")[1].Split("</html")[1];
return before + "<html>...REDACTED...</html" + afterEnd.Trim();
}
private static string SplitLongWords(this string value, int maxLength = 200)
{
foreach (var word in value.Split('\n').SelectMany(l => l.Split(' ')).Where(w => w.Length > maxLength))
value = value.Replace(word, string.Join("\n", word.Chunk(maxLength).Select(c => new string(c))));
return value;
}
}Wire it up:
// In your test setup / report configuration
new ReportConfigurationOptions
{
SpecificationsTitle = "My API Specifications",
RequestResponsePostProcessor = DiagramContentProcessor.PostProcessor,
ExcludedHeaders = ["X-Request-Id", "X-Correlation-Id"]
}The order you chain redaction steps matters. Recommended order:
-
Extract useful metadata from tokens (e.g.
ExposeAuthLevels) — do this before redacting, so you can still read the JWT -
Redact tokens and secrets (
RedactMiddle/RedactEnding) — remove sensitive values - Redact cookies and session data — reduce noise from large cookie headers
- Redact large HTML — prevent oversized diagrams from identity provider responses
- Split long words — break any remaining long strings so PlantUML can render them
If you redact tokens before extracting claims, the JWT will already be truncated and unreadable.
- Test your processor by running your test suite and checking the generated PlantUML. If diagrams are still too wide or fail to render, you likely have unredacted long values.
-
Use
RedactMiddlefor tokens — keeping the start and end visible makes it possible to correlate which token is which across diagram notes. -
Use
RedactEndingfor cookies and session data where only the name/start matters. - Set a size threshold for HTML redaction (e.g. 3,000 characters) so small HTML fragments still appear but full pages are collapsed.
- Pre-processors vs post-processors: Use pre-processors only when you need to transform the raw body before the library's JSON pretty-printer runs (e.g. decrypting, decompressing, parsing XML). For everything else, use post-processors — they operate on the final formatted text which is more predictable to regex against.
Getting Started
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration ClickHouse Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Search Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
- Merging Parallel Reports
Reference