Proxyman icon indicating copy to clipboard operation
Proxyman copied to clipboard

CustomTabs examples

Open iliakan opened this issue 8 months ago • 4 comments

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;
}

iliakan avatar Apr 15 '25 20:04 iliakan

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

NghiaTranUIT avatar Apr 16 '25 01:04 NghiaTranUIT

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 avatar Apr 16 '25 05:04 iliakan

@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 avatar Jun 05 '25 06:06 johnib

@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

NghiaTranUIT avatar Jun 05 '25 06:06 NghiaTranUIT