---
title: "Parse HTML from a string, file or stream"
url: https://recursiveai.org/onehtml/cookbook/parse-html/
description: "Load HTML into a document from a string, a file or a stream, let the parser pick the encoding from the bytes, and see the tokenizer’s parse errors when…"
version: "1.0.0"
---

# Parse HTML from a string, file or stream

Load HTML into a document from a string, a file or a stream, let the parser pick the encoding from the bytes, and see the tokenizer’s parse errors when you need them.

`Document.Parse` has three overloads: bytes (`ReadOnlyMemory<byte>`), a `string`, and an already decoded `SourceText`. There is no `Stream` or file-path overload, because the library never reads files or the network. You read the input and pass the bytes.

Prefer bytes whenever you have them. With bytes, the parser applies the WHATWG encoding rules: byte order mark, then the transport `Content-Type` (`ParseOptions.ContentType`), then the `<meta>` prescan. A string is used as it is.

```csharp
using System.Text;
using RecursiveAI.OneHtml;

// From a string
Document fromString = Document.Parse("<p class=lead>Hello, <b>world</b>");
Console.WriteLine(fromString.Root.QuerySelector("p.lead").TextContent);
Console.WriteLine(fromString.SerializeNode(fromString.DocumentElement));

// From a file: read the bytes and let the parser sniff the encoding
Document fromFile = Document.Parse(File.ReadAllBytes("events.html"));
Console.WriteLine($"{fromFile.Source.Encoding.Name}: {fromFile.Root.QuerySelector("h1").TextContent}");

// From a stream: copy it into memory first; Parse takes bytes, not a Stream
using Stream stream = File.OpenRead("events.html");
using var buffer = new MemoryStream();
await stream.CopyToAsync(buffer);
Document fromStream = Document.Parse(buffer.GetBuffer().AsMemory(0, (int)buffer.Length));
Console.WriteLine($"{fromStream.NodeCount} nodes");

// Tokenizer errors are recorded only when asked for
var options = new ParseOptions { RecordErrors = true };
Document broken = Document.Parse("<p title=\"a\"b>Fish &chips</p>", options);
foreach (var error in broken.ParseErrors)
{
    Console.WriteLine($"{error.Error} at offset {error.Offset}");
}
Console.WriteLine(broken.SerializeNode(broken.Root.QuerySelector("p")));
```

```text
Hello, world
<html><head></head><body><p class="lead">Hello, <b>world</b></p></body></html>
UTF-8: Upcoming events
101 nodes
MissingWhitespaceBetweenAttributes at offset 12
<p title="a" b="">Fish &amp;chips</p>
```

Some things to know:

- A string input still gets the full document: `<html>`, `<head>` and `<body>` are created as a browser would create them. To parse a snippet without them, see [HTML fragments](https://recursiveai.org/onehtml/cookbook/html-fragments/).
- `buffer.GetBuffer().AsMemory(0, (int)buffer.Length)` hands over the stream’s bytes without a second copy. `buffer.ToArray()` works too.
- Parse errors never throw. They are recorded only when `ParseOptions.RecordErrors` is set, and only the tokenizer’s errors are recorded (not tree construction errors). Without it, `ParseErrors` is empty.
- Hostile-input limits (input size, nesting depth, attribute count) are checked before the work they guard. Going past one throws `HtmlLimitExceededException`. Set your own with `ParseOptions.Limits`.
- `SerializeNode` and `SerializeChildren` write the HTML serialization of the tree. To write the page back with its original bytes, use `ToLosslessBytes()` (see [Edit HTML without reformatting it](https://recursiveai.org/onehtml/cookbook/edit-html-csharp-lossless/)).

Encoding detection in detail: [Detect the encoding of an HTML page](https://recursiveai.org/onehtml/cookbook/detect-encoding/).

A longer guide with encoding detection and broken markup: [Parse HTML in C#](https://recursiveai.org/onehtml/articles/parse-html-csharp/).

Fetching, encodings, robots.txt and JavaScript-built pages: [Web scraping in C#](https://recursiveai.org/onehtml/articles/web-scraping-csharp/).
