CustomTabs examples
Here're two quick examples of formatting functions for custom tabs. Just wrote them, tested a tiny bit =)
RawBody
- No headers
- Non-printable characters become .
- Supports mixed binary + UTF8
/**
* Formats a Uint8Array or string containing mixed binary data and UTF-8 text
* Non-printable characters are replaced with dots
*
* @param {Uint8Array|string} data - The binary data to format
* @returns {string} Text representation of the data with non-printable characters as dots
*/
function formatBinaryData(data) {
if (!data || data.length === 0) {
return '';
}
// Convert string input to Uint8Array if needed
if (typeof data === 'string') {
// Convert string to array of character codes
const charCodes = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
charCodes[i] = data.charCodeAt(i) & 0xFF; // Get the byte value
}
data = charCodes;
}
// Process the Uint8Array manually without TextDecoder
let result = '';
let i = 0;
while (i < data.length) {
const byte = data[i];
// Check if this is the start of a UTF-8 multi-byte sequence
if ((byte & 0xE0) === 0xC0 && i + 1 < data.length) { // 2-byte sequence
// Ensure the continuation byte is valid
if ((data[i + 1] & 0xC0) === 0x80) {
// Decode 2-byte UTF-8 sequence manually
const codePoint = ((byte & 0x1F) << 6) | (data[i + 1] & 0x3F);
if (codePoint >= 0x80) { // Valid code point
result += String.fromCharCode(codePoint);
i += 2;
continue;
}
}
// Invalid sequence
result += '.';
i++;
} else if ((byte & 0xF0) === 0xE0 && i + 2 < data.length) { // 3-byte sequence
// Ensure the continuation bytes are valid
if ((data[i + 1] & 0xC0) === 0x80 && (data[i + 2] & 0xC0) === 0x80) {
// Decode 3-byte UTF-8 sequence manually
const codePoint = ((byte & 0x0F) << 12) |
((data[i + 1] & 0x3F) << 6) |
(data[i + 2] & 0x3F);
if (codePoint >= 0x800 && (codePoint < 0xD800 || codePoint > 0xDFFF)) { // Valid code point
result += String.fromCharCode(codePoint);
i += 3;
continue;
}
}
// Invalid sequence
result += '.';
i++;
} else if ((byte & 0xF8) === 0xF0 && i + 3 < data.length) { // 4-byte sequence
// Ensure the continuation bytes are valid
if ((data[i + 1] & 0xC0) === 0x80 &&
(data[i + 2] & 0xC0) === 0x80 &&
(data[i + 3] & 0xC0) === 0x80) {
// Decode 4-byte UTF-8 sequence manually
const codePoint = ((byte & 0x07) << 18) |
((data[i + 1] & 0x3F) << 12) |
((data[i + 2] & 0x3F) << 6) |
(data[i + 3] & 0x3F);
if (codePoint >= 0x10000 && codePoint <= 0x10FFFF) { // Valid code point
// For code points above 0xFFFF, we need to use surrogate pairs
const highSurrogate = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800;
const lowSurrogate = ((codePoint - 0x10000) % 0x400) + 0xDC00;
result += String.fromCharCode(highSurrogate, lowSurrogate);
i += 4;
continue;
}
}
// Invalid sequence
result += '.';
i++;
} else if (byte >= 32 && byte <= 126) { // ASCII printable
result += String.fromCharCode(byte);
i++;
} else if (byte === 10) { // Newline character (\n)
result += '\n';
i++;
} else { // Non-printable
result += '.';
i++;
}
}
return result;
}
function onRequest(context, url, request) {
let rawRequest = '';
try {
// If it's an array of bytes, decode as UTF-8 (like raw text)
try {
const decoded = formatBinaryData(request.rawBody);
rawRequest += decoded;
} catch(e) {
rawRequest += String(request.rawBody);
}
} catch (e) {
rawRequest += e.message;
}
// Add to custom previewer tab
request.customPreviewerTabs["RawBody"] = rawRequest;
return request;
}
function onResponse(context, url, request, response) {
response.customPreviewerTabs['RawBody'] = formatBinaryData(response.rawBody);
return response;
}
EventSource
Makes it JSON (more readable)
function formatEventStream(rawStream) {
const lines = rawStream.split('\n');
let events = [];
for (const line of lines) {
if (line.startsWith('event: ')) {
const event = line.slice(7);
events.push({type: event, data: []});
} else if (line.startsWith('data: ')) {
let data = line.slice(6);
try {
data = JSON.parse(data);
} catch (e) {
// ignore
}
if (data == '[DONE]') break;
events.at(-1).data.push(data);
}
}
return events;
}
async function onResponse(context, url, request, response) {
if (!response.headers["Content-Type"] || !response.headers["Content-Type"].includes('event-stream')) {
return response;
}
try {
const formatted = formatEventStream(response.rawBody);
response.customPreviewerTabs['EventStream'] = JSON.stringify(formatted, null, 2);
} catch(e) {
response.customPreviewerTabs['EventStream'] = e.message;
}
return response;
}
Thanks, but it's too complicated to convert from Uint8Array to String. Most of the time, your Body is just a string or JSON. You can serialize it to a string easily by using JSON.stringify()
If it's Uint8Array, don't try to convert it to text because it will contain non-reading chars. Just add <raw body> and continue with the Header
In my case, I have a mixed binary and string output because the format is unknown, it looks like a protobuf without description, but protobuf viewer fails to display it well.
So this formatter aims to output it as good as possible.
@iliakan thanks for sharing this. I am on the latest Proxyman version: 5.20.0 and I can't get the Previewer tabs to work on SSE responses.
I am trying to put in the previewer tab a simple JSON which is returned as part of SSE response (the usual format of /completions API of LM providers such as OpenAI, Anthropic, etc.)
I am able to log that JSON to the Proxyman console, via my script, but putting that same JSON into a previewer tab -- does not work. It keeps saying "Invalid data. Could not display as a String".
Even putting a constant value (JSON or string) does not work.
Very weird.
@johnib Let me implement the native custom tab, for SEE OpenAPI. It will prettify each JSON Event automatically. No need to use script 👍
Will send the BETA this week