current commit
260406.2 · inspector: return errors when CDP protocol event emission fails
// Flags: --inspect=0 --experimental-network-inspection --experimental-storage-inspection
'use strict';
const common = require('../common');

common.skipIfInspectorDisabled();

const inspector = require('node:inspector/promises');
const assert = require('node:assert');

function omit(object, ...keys) {
  const copy = { ...object };
  for (const key of keys) {
    delete copy[key];
  }
  return copy;
}

function networkRequest(overrides = {}) {
  return {
    requestId: 'request-id',
    timestamp: 1000,
    wallTime: 1000,
    request: {
      url: 'https://nodejs.org/en',
      method: 'GET',
      headers: {},
    },
    ...overrides,
  };
}

function networkResponse(overrides = {}) {
  return {
    requestId: 'response-id',
    timestamp: 1000,
    type: 'Other',
    response: {
      url: 'https://nodejs.org/en',
      status: 200,
      statusText: 'OK',
      headers: {},
    },
    ...overrides,
  };
}

function loadingFailed(overrides = {}) {
  return {
    requestId: 'loading-failed-id',
    timestamp: 1000,
    type: 'Document',
    errorText: 'Failed to load resource',
    ...overrides,
  };
}

function loadingFinished(overrides = {}) {
  return {
    requestId: 'loading-finished-id',
    timestamp: 1000,
    ...overrides,
  };
}

function webSocketCreated(overrides = {}) {
  return {
    requestId: 'websocket-created-id',
    url: 'ws://example.com:8080',
    ...overrides,
  };
}

function webSocketClosed(overrides = {}) {
  return {
    requestId: 'websocket-closed-id',
    timestamp: 1000,
    ...overrides,
  };
}

function webSocketResponse(overrides = {}) {
  return {
    requestId: 'websocket-response-id',
    timestamp: 1000,
    response: {
      status: 101,
      statusText: 'Switching Protocols',
      headers: {},
    },
    ...overrides,
  };
}

function storageId(overrides = {}) {
  return {
    securityOrigin: '',
    isLocalStorage: true,
    storageKey: 'node-inspector://default-dom-storage',
    ...overrides,
  };
}

function domStorageItemAdded(overrides = {}) {
  return {
    storageId: storageId(),
    key: 'testKey',
    newValue: 'testValue',
    ...overrides,
  };
}

function domStorageItemRemoved(overrides = {}) {
  return {
    storageId: storageId(),
    key: 'testKey',
    ...overrides,
  };
}

function domStorageItemUpdated(overrides = {}) {
  return {
    storageId: storageId(),
    key: 'testKey',
    oldValue: 'oldValue',
    newValue: 'newValue',
    ...overrides,
  };
}

function registerStorage(overrides = {}) {
  return {
    isLocalStorage: true,
    storageMap: {},
    ...overrides,
  };
}

const NETWORK_ERROR_CASES = [
  [
    'requestWillBeSent',
    omit(networkRequest(), 'requestId'),
    'Missing requestId in event',
  ],
  [
    'requestWillBeSent',
    omit(networkRequest(), 'timestamp'),
    'Missing timestamp in event',
  ],
  [
    'requestWillBeSent',
    omit(networkRequest(), 'wallTime'),
    'Missing wallTime in event',
  ],
  [
    'requestWillBeSent',
    omit(networkRequest(), 'request'),
    'Missing request in event',
  ],
  [
    'requestWillBeSent',
    networkRequest({ request: omit(networkRequest().request, 'url') }),
    'Missing request.url in event',
  ],
  [
    'requestWillBeSent',
    networkRequest({ request: omit(networkRequest().request, 'method') }),
    'Missing request.method in event',
  ],
  [
    'requestWillBeSent',
    networkRequest({ request: omit(networkRequest().request, 'headers') }),
    'Missing request.headers in event',
  ],

  [
    'responseReceived',
    omit(networkResponse(), 'requestId'),
    'Missing requestId in event',
  ],
  [
    'responseReceived',
    omit(networkResponse(), 'timestamp'),
    'Missing timestamp in event',
  ],
  [
    'responseReceived',
    omit(networkResponse(), 'type'),
    'Missing type in event',
  ],
  [
    'responseReceived',
    omit(networkResponse(), 'response'),
    'Missing response in event',
  ],
  [
    'responseReceived',
    networkResponse({ response: omit(networkResponse().response, 'url') }),
    'Missing response.url in event',
  ],
  [
    'responseReceived',
    networkResponse({ response: omit(networkResponse().response, 'status') }),
    'Missing response.status in event',
  ],
  [
    'responseReceived',
    networkResponse({
      response: omit(networkResponse().response, 'statusText'),
    }),
    'Missing response.statusText in event',
  ],
  [
    'responseReceived',
    networkResponse({ response: omit(networkResponse().response, 'headers') }),
    'Missing response.headers in event',
  ],
  [
    'responseReceived',
    networkResponse({
      response: { ...networkResponse().response, headers: { host: 1 } },
    }),
    'Invalid header value in event',
  ],

  [
    'loadingFailed',
    omit(loadingFailed(), 'requestId'),
    'Missing requestId in event',
  ],
  [
    'loadingFailed',
    omit(loadingFailed(), 'timestamp'),
    'Missing timestamp in event',
  ],
  ['loadingFailed', omit(loadingFailed(), 'type'), 'Missing type in event'],
  [
    'loadingFailed',
    omit(loadingFailed(), 'errorText'),
    'Missing errorText in event',
  ],

  [
    'loadingFinished',
    omit(loadingFinished(), 'requestId'),
    'Missing requestId in event',
  ],
  [
    'loadingFinished',
    omit(loadingFinished(), 'timestamp'),
    'Missing timestamp in event',
  ],

  [
    'webSocketCreated',
    omit(webSocketCreated(), 'requestId'),
    'Missing requestId in event',
  ],
  ['webSocketCreated', omit(webSocketCreated(), 'url'), 'Missing url in event'],

  [
    'webSocketClosed',
    omit(webSocketClosed(), 'requestId'),
    'Missing requestId in event',
  ],
  [
    'webSocketClosed',
    omit(webSocketClosed(), 'timestamp'),
    'Missing timestamp in event',
  ],

  [
    'webSocketHandshakeResponseReceived',
    omit(webSocketResponse(), 'requestId'),
    'Missing requestId in event',
  ],
  [
    'webSocketHandshakeResponseReceived',
    omit(webSocketResponse(), 'timestamp'),
    'Missing timestamp in event',
  ],
  [
    'webSocketHandshakeResponseReceived',
    omit(webSocketResponse(), 'response'),
    'Missing response in event',
  ],
  [
    'webSocketHandshakeResponseReceived',
    webSocketResponse({
      response: omit(webSocketResponse().response, 'status'),
    }),
    'Missing response.status in event',
  ],
  [
    'webSocketHandshakeResponseReceived',
    webSocketResponse({
      response: omit(webSocketResponse().response, 'statusText'),
    }),
    'Missing response.statusText in event',
  ],
  [
    'webSocketHandshakeResponseReceived',
    webSocketResponse({
      response: omit(webSocketResponse().response, 'headers'),
    }),
    'Missing response.headers in event',
  ],
];

const DOM_STORAGE_ERROR_CASES = [
  [
    'domStorageItemAdded',
    omit(domStorageItemAdded(), 'storageId'),
    'Missing storageId in event',
  ],
  [
    'domStorageItemAdded',
    domStorageItemAdded({ storageId: omit(storageId(), 'securityOrigin') }),
    'Missing securityOrigin in storageId',
  ],
  [
    'domStorageItemAdded',
    domStorageItemAdded({ storageId: omit(storageId(), 'storageKey') }),
    'Missing storageKey in storageId',
  ],
  [
    'domStorageItemAdded',
    omit(domStorageItemAdded(), 'key'),
    'Missing key in event',
  ],
  [
    'domStorageItemAdded',
    omit(domStorageItemAdded(), 'newValue'),
    'Missing newValue in event',
  ],

  [
    'domStorageItemRemoved',
    omit(domStorageItemRemoved(), 'storageId'),
    'Missing storageId in event',
  ],
  [
    'domStorageItemRemoved',
    omit(domStorageItemRemoved(), 'key'),
    'Missing key in event',
  ],

  [
    'domStorageItemUpdated',
    omit(domStorageItemUpdated(), 'storageId'),
    'Missing storageId in event',
  ],
  [
    'domStorageItemUpdated',
    omit(domStorageItemUpdated(), 'key'),
    'Missing key in event',
  ],
  [
    'domStorageItemUpdated',
    omit(domStorageItemUpdated(), 'oldValue'),
    'Missing oldValue in event',
  ],
  [
    'domStorageItemUpdated',
    omit(domStorageItemUpdated(), 'newValue'),
    'Missing newValue in event',
  ],

  ['domStorageItemsCleared', {}, 'Missing storageId in event'],

  [
    'registerStorage',
    omit(registerStorage(), 'isLocalStorage'),
    'Missing isLocalStorage in event',
  ],
  [
    'registerStorage',
    omit(registerStorage(), 'storageMap'),
    'Missing storageMap in event',
  ],
  [
    'registerStorage',
    registerStorage({
      storageMap: new Proxy(
        {},
        {
          ownKeys() {
            throw new Error('boom');
          },
        },
      ),
    }),
    'Failed to get property names from storageMap',
  ],
  [
    'registerStorage',
    registerStorage({
      storageMap: new Proxy(
        { testKey: 'testValue' },
        {
          get(target, property, receiver) {
            if (property === 'testKey') throw new Error('boom');
            return Reflect.get(target, property, receiver);
          },
        },
      ),
    }),
    'Failed to get value from storageMap',
  ],
];

const DATA_SENT_REQUEST_ID = 'data-sent-id';
const DATA_SENT_ERROR_CASES = [
  [{ finished: false }, 'Missing requestId in event'],
  [{ requestId: DATA_SENT_REQUEST_ID }, 'Missing timestamp in event'],
  [
    { requestId: DATA_SENT_REQUEST_ID, timestamp: 1000 },
    'Missing dataLength in event',
  ],
  [
    { requestId: DATA_SENT_REQUEST_ID, timestamp: 1000, dataLength: 1 },
    'Missing data in event',
  ],
  [
    {
      requestId: DATA_SENT_REQUEST_ID,
      timestamp: 1000,
      dataLength: 1,
      data: {},
    },
    'Expected data to be Uint8Array in event',
  ],
];

const DATA_RECEIVED_ERROR_CASES = [
  [{}, 'Missing requestId in event'],
  [{ requestId: 'data-received-id' }, 'Missing timestamp in event'],
  [
    { requestId: 'data-received-id', timestamp: 1000 },
    'Missing dataLength in event',
  ],
  [
    { requestId: 'data-received-id', timestamp: 1000, dataLength: 1 },
    'Missing encodedDataLength in event',
  ],
  [
    {
      requestId: 'data-received-id',
      timestamp: 1000,
      dataLength: 1,
      encodedDataLength: 1,
    },
    'Missing data in event',
  ],
  [
    {
      requestId: 'data-received-id',
      timestamp: 1000,
      dataLength: 1,
      encodedDataLength: 1,
      data: {},
    },
    'Expected data to be Uint8Array in event',
  ],
];

function assertEventErrors(domain, name, params, message) {
  assert.throws(
    () => inspector[domain][name](params),
    {
      message,
    },
    `Expected ${domain}.${name} to throw`,
  );
}

function startRequest(requestId) {
  inspector.Network.requestWillBeSent(networkRequest({ requestId }));
}

(async () => {
  const session = new inspector.Session();
  session.connect();

  await session.post('Network.enable');
  await session.post('DOMStorage.enable');

  for (const [name, params, message] of NETWORK_ERROR_CASES) {
    assertEventErrors('Network', name, params, message);
  }

  startRequest(DATA_SENT_REQUEST_ID);
  for (const [params, message] of DATA_SENT_ERROR_CASES) {
    assertEventErrors('Network', 'dataSent', params, message);
  }

  for (const [params, message] of DATA_RECEIVED_ERROR_CASES) {
    assertEventErrors('Network', 'dataReceived', params, message);
  }

  for (const [name, params, message] of DOM_STORAGE_ERROR_CASES) {
    assertEventErrors('DOMStorage', name, params, message);
  }
})().then(common.mustCall());