Mastering RFC 4180: The Official CSV Specification & Edge Cases

RFC 4180 CSV Specification and Edge Cases

Every developer starts out thinking CSV is the easiest format in the world. It’s just comma-separated text, right? You write a quick loop, call line.split(','), and move on.

Then real life happens.

A customer enters an address like "Apt 4, 100 Main St", and your script shifts their postal code into the country column. A user submits feedback with line breaks, and your parser creates three ghost records. An exec opens the export in Excel, and all the accented letters turn into garbled hieroglyphics.

CSV looks simple, but under the hood it’s packed with forty years of historical quirks. For decades, software tools just made up their own parsing rules until the IETF finally stepped in with RFC 4180. Here’s what the spec actually says, the edge cases that crash real systems, and how to build an airtight state-machine parser in pure JavaScript.

Why Did CSV Need an RFC in the First Place?

Back in the 1980s and 90s, every spreadsheet program—from VisiCalc and Lotus 1-2-3 to early Microsoft Excel—invented its own custom flavor of delimited text. Some escaped quotes with backslashes. Some used single quotes. Many banned newlines inside cells entirely.

By 2005, the explosion of web APIs made the lack of a common standard a nightmare. The IETF introduced RFC 4180 to establish one definitive grammar for the text/csv MIME type.

The Seven Practical Rules of RFC 4180

Strip away the formal specification jargon, and RFC 4180 boils down to seven simple rules:

  1. Line Breaks (CRLF): Every record ends with a Carriage Return and Line Feed (\r\n). While Unix systems often output bare \n, compliant emitters should always write \r\n.
  2. Optional Header: The first line can be a header row formatted just like data rows.
  3. Uniform Columns: Every single row must have the exact same number of fields. No jagged rows allowed.
  4. Spaces Matter: Leading and trailing spaces belong to the field data. Parsers aren't allowed to strip them automatically.
  5. When to Quote: If a field contains a comma (,), a double quote ("), or a line break (\r\n), the entire value must be wrapped in double quotes.
  6. The Escaped Quote Rule: If an enclosed field contains a literal double quote, escape it by doubling the quote character ("").
  7. Unquoted Text: Plain text without commas or line breaks doesn't need quotes, though quotes are always allowed.

The Backslash Anti-Pattern (And Why It Breaks Parsers)

The most common bug developers introduce when generating CSVs is borrowing C or JSON backslash escaping (\").

Say a user's name is Robert "Bob" Smith. A JavaScript developer instinctively writes:

// WRONG (Violates RFC 4180)
"Robert \"Bob\" Smith",32,Engineer

When an RFC 4180 parser (like Postgres COPY, Python's csv module, or Excel) reads this line, it sees the backslash as plain text. Then it hits the quote after Bob, assumes the cell is finished, and crashes when it hits Smith".

The correct syntax is:

// CORRECT (RFC 4180 Compliant)
"Robert ""Bob"" Smith",32,Engineer
Quick Rule: In standard CSV, double quotes escape double quotes. Whenever you want one quote inside a cell, write two. For example, """Hello""" represents the text "Hello".

Multiline Fields: When Newlines Live Inside Cells

Can a single CSV cell contain line breaks? Yes, absolutely.

Look at this customer support ticket export:

ticket_id,customer_name,issue_description,priority
101,"Jane Doe","App crashed on startup.
Steps to reproduce:
1. Open settings
2. Click sync",High
102,"Mark Vance","Cannot reset password",Low

Even though this file has 7 physical lines of text, it represents exactly 2 data records. Because the issue_description field opens with a quote on line 2 and doesn't close until after step 2, a compliant parser treats every newline inside as literal text, not a new row.

Building a Bulletproof State Machine Parser

Because quotes and line breaks can appear anywhere, regex and split() are useless for real-world CSV. You need a character scanner implemented as a Finite State Machine (FSM).

Here is a zero-dependency, high-performance implementation in JavaScript:

RFC 4180 State Machine Parser (JavaScript)
/**
 * High-performance RFC 4180 CSV parser using a 5-state scanner.
 */
function parseRFC4180(text, delimiter = ',') {
  const rows = [];
  let currentRow = [];
  let currentCell = '';
  let inQuotes = false;
  let i = 0;

  while (i < text.length) {
    const char = text[i];
    const nextChar = text[i + 1];

    if (inQuotes) {
      if (char === '"') {
        if (nextChar === '"') {
          // Escaped quote: "" -> "
          currentCell += '"';
          i += 2;
          continue;
        } else {
          // Closing quote
          inQuotes = false;
          i++;
          continue;
        }
      } else {
        // Everything inside quotes is literal data
        currentCell += char;
        i++;
      }
    } else {
      // Outside quotes
      if (char === '"') {
        inQuotes = true;
        i++;
      } else if (char === delimiter) {
        // Field boundary
        currentRow.push(currentCell);
        currentCell = '';
        i++;
      } else if (char === '\r' && nextChar === '\n') {
        // CRLF Record boundary
        currentRow.push(currentCell);
        rows.push(currentRow);
        currentRow = [];
        currentCell = '';
        i += 2;
      } else if (char === '\n' || char === '\r') {
        // Lone LF/CR Record boundary
        currentRow.push(currentCell);
        rows.push(currentRow);
        currentRow = [];
        currentCell = '';
        i++;
      } else {
        currentCell += char;
        i++;
      }
    }
  }

  if (currentCell.length > 0 || currentRow.length > 0) {
    currentRow.push(currentCell);
    rows.push(currentRow);
  }

  return rows;
}

The Excel UTF-8 Nightmare (The 3-Byte Fix)

If you've ever exported data containing international names (like "Renée" or "Müller") and had users complain that Excel displayed "Renée", you’ve run into Excel's legacy encoding quirk.

On Windows, Excel opens CSV files using the old Windows-1252 ANSI code page unless it sees a Byte Order Mark (BOM) at the very start of the file.

To fix this, prepend the 3-byte UTF-8 BOM (\uFEFF) to your CSV blob before exporting:

Exporting with UTF-8 BOM
function exportCsvWithBom(csvText, filename = 'export.csv') {
  // Prepending \uFEFF forces Excel into UTF-8 mode
  const BOM = '\uFEFF';
  const blob = new Blob([BOM + csvText], { type: 'text/csv;charset=utf-8;' });

  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

Security: Beware of CSV Formula Injection (DDE)

Security Alert: If your web app lets users submit free text (like usernames or comments) and later lets admins export that data to CSV, attackers can trigger CSV Formula Injection.

If an attacker sets their profile name to =CMD|' /C calc.exe'!A0, spreadsheet applications can interpret the cell as an executable command. When an admin opens the file, Excel prompts to run the command, potentially compromising their machine.

To sanitize against formula injection, check if the first character begins with =, +, -, @, or a tab. If so, prepend a single quote ('):

function sanitizeFormulaInjection(val) {
  if (typeof val !== 'string') return val;
  const triggers = ['=', '+', '-', '@', '\t', '\r'];
  if (triggers.some(t => val.startsWith(t))) {
    return `'${val}`; // Forces spreadsheets to treat cell as plain string
  }
  return val;
}

Quick Checklist for Clean CSV Exports

Our local-first JSON to CSV Converter Studio implements full RFC 4180 parsing, automatic BOM prepending, and formula sanitization in memory.