Tình huống gặp phải
Bạn đang gọi API, đọc file XML, hoặc xử lý response — và gặp phải lỗi này:
Warning: SimpleXMLElement::__construct(): Entity: line 1: parser error : Start tag expected, '<' not found in /var/www/html/app/XmlParser.php on line 42
Code của bạn trông có thể như thế này:
<?php
$response = file_get_contents('https://api.example.com/feed.xml');
$xml = new SimpleXMLElement($response); // <-- lỗi xảy ra ở đây
Hoặc dạng hàm:
$xml = simplexml_load_string($response);
Nguyên nhân gốc rễ hầu như luôn là một: bạn đang truyền vào parser thứ gì đó không phải XML.
Tại sao lỗi này xảy ra
XML parser của PHP đọc ký tự đầu tiên và mong đợi thấy <. Bất kỳ thứ gì khác — khoảng trắng, byte lạ, một chữ cái — đều kích hoạt cảnh báo này. Dưới đây là các nguyên nhân thường gặp:
- API trả về trang HTML lỗi — response 500, trang chuyển hướng đăng nhập, hoặc trang Cloudflare challenge (403) thay vì XML
- Response rỗng — URL không trả về gì, hoặc
file_get_contents()thất bại âm thầm - Ký tự BOM — một UTF-8 BOM (
\xEF\xBB\xBF) được chèn vào đầu chuỗi XML, khiến parser thấy 3 byte vô hình trước< - Khoảng trắng trước khai báo XML — một số trình tạo file thêm dòng trống trước
<?xml version="1.0"?> - Biến chứa thông báo lỗi — có gì đó phía trước thất bại và gán
$responsethành chuỗi như"Error: timeout" - HTML entities ở sai chỗ — chuỗi entity bị decode hai lần hoặc bị lỗi định dạng
Chẩn đoán trước — in ra dữ liệu thực tế bạn đang có
Trước khi sửa bất cứ thứ gì, hãy in dữ liệu thô ra để biết mình đang xử lý cái gì:
$response = file_get_contents('https://api.example.com/feed.xml');
var_dump(substr($response, 0, 200)); // 200 ký tự đầu tiên
var_dump(strlen($response)); // có rỗng không?
Xem kết quả trả về. string(0) "" nghĩa là fetch thất bại. <!DOCTYPE html> nghĩa là bạn nhận được trang HTML. Độ dài bằng 3 nhưng trông như rỗng? Gần như chắc chắn đó là BOM.
Sửa nhanh: kiểm tra trước khi parse
Bọc phần parse trong một hàm kiểm tra an toàn để xử lý ba trường hợp lỗi phổ biến nhất — chuỗi rỗng, nội dung không phải XML, và BOM:
<?php
function safe_parse_xml(string $raw): ?SimpleXMLElement {
// Xóa UTF-8 BOM nếu có
$raw = ltrim($raw, "\xEF\xBB\xBF");
// Xóa khoảng trắng đầu (một số feed có \n trước <?xml)
$raw = ltrim($raw);
// Thoát nếu chuỗi rỗng
if (empty($raw)) {
error_log('XML parse failed: empty input');
return null;
}
// Thoát nếu không có vẻ là XML
if ($raw[0] !== '<') {
error_log('XML parse failed: input does not start with < — got: ' . substr($raw, 0, 100));
return null;
}
// Tắt cảnh báo và kiểm tra giá trị trả về
libxml_use_internal_errors(true);
$xml = simplexml_load_string($raw);
if ($xml === false) {
$errors = libxml_get_errors();
foreach ($errors as $error) {
error_log('libxml error: ' . trim($error->message));
}
libxml_clear_errors();
return null;
}
return $xml;
}
// Sử dụng
$response = file_get_contents('https://api.example.com/feed.xml');
$xml = safe_parse_xml($response);
if ($xml === null) {
// xử lý thất bại — hiển thị thông báo thân thiện, thử lại, v.v.
throw new RuntimeException('Could not parse XML response');
}
Hai thứ làm cho hàm này hoạt động: libxml_use_internal_errors(true) tắt cảnh báo PHP, và libxml_get_errors() cho bạn biết lỗi parse thực sự là gì. Nếu không có chúng, cảnh báo sẽ làm ô nhiễm error log hoặc rò rỉ ra trình duyệt — và bạn vẫn không có gì có cấu trúc để xử lý.
Sửa lỗi dành riêng cho trường hợp BOM
Các file được lưu bởi công cụ Windows — Notepad, xuất Excel, bất cứ thứ gì từ stack Microsoft cũ — thường chứa UTF-8 BOM. Bạn cũng gặp trường hợp này khi đọc từ cột database được nhập bởi Windows client.
// Kiểm tra BOM
if (substr($raw, 0, 3) === "\xEF\xBB\xBF") {
$raw = substr($raw, 3);
}
Cách dùng ltrim trong hàm ở trên linh hoạt hơn khi bạn có hỗn hợp khoảng trắng đầu và BOM. Cách kiểm tra tường minh ở trên gọn gàng hơn khi bạn chỉ nhắm vào BOM.
Sửa lỗi khi API response trả về HTML lỗi
Luôn kiểm tra HTTP status code trước khi cố parse body:
$context = stream_context_create([
'http' => [
'ignore_errors' => true, // không thất bại khi gặp 4xx/5xx
]
]);
$response = file_get_contents('https://api.example.com/feed.xml', false, $context);
// $http_response_header được tự động điền bởi file_get_contents
if (isset($http_response_header[0]) && strpos($http_response_header[0], '200') === false) {
throw new RuntimeException('API returned non-200: ' . $http_response_header[0]);
}
$xml = safe_parse_xml($response);
Trong môi trường production, cURL là lựa chọn tốt hơn file_get_contents — dùng curl_getinfo($ch, CURLINFO_HTTP_CODE) để kiểm tra status trước khi đụng vào body.
Sửa triệt để: tiện ích load XML mạnh mẽ
Đặt đoạn này vào một class tiện ích dùng chung và bạn sẽ không bao giờ phải copy-paste logic kiểm tra nữa:
<?php
class XmlLoader {
/**
* Parse chuỗi XML an toàn, trả về null nếu có bất kỳ lỗi nào.
* Xử lý BOM, khoảng trắng đầu, input rỗng, và lỗi libxml.
*/
public static function parse(string $raw, bool $logErrors = true): ?SimpleXMLElement {
$raw = ltrim($raw, "\xEF\xBB\xBF"); // xóa BOM
$raw = ltrim($raw); // xóa khoảng trắng đầu
if ($raw === '' || $raw[0] !== '<') {
if ($logErrors) {
error_log('[XmlLoader] Invalid input: ' . substr($raw, 0, 80));
}
return null;
}
libxml_use_internal_errors(true);
$xml = simplexml_load_string($raw, SimpleXMLElement::class, LIBXML_NOCDATA);
if ($xml === false) {
if ($logErrors) {
foreach (libxml_get_errors() as $err) {
error_log('[XmlLoader] ' . trim($err->message) . ' (line ' . $err->line . ')');
}
}
libxml_clear_errors();
return null;
}
return $xml;
}
/**
* Load và parse file XML một cách an toàn.
*/
public static function parseFile(string $path): ?SimpleXMLElement {
if (!is_readable($path)) {
error_log('[XmlLoader] File not readable: ' . $path);
return null;
}
return self::parse(file_get_contents($path));
}
}
Flag LIBXML_NOCDATA là một tính năng hữu ích đi kèm — nó tự động chuyển đổi các phần CDATA thành chuỗi thuần, giúp bạn không cần cast node thủ công mỗi lần truy cập.
Kiểm tra lại xem fix có hoạt động không
Chạy kiểm tra nhanh với cả input hợp lệ và không hợp lệ:
// Phải trả về SimpleXMLElement
$good = XmlLoader::parse('<?xml version="1.0"?><root><item>test</item></root>');
var_dump($good instanceof SimpleXMLElement); // bool(true)
// Phải trả về null (không throw)
$bad = XmlLoader::parse('');
var_dump($bad); // NULL
$alsoBad = XmlLoader::parse('Error: service unavailable');
var_dump($alsoBad); // NULL
// Trường hợp BOM
$bom = "\xEF\xBB\xBF" . '<root/>';
$result = XmlLoader::parse($bom);
var_dump($result instanceof SimpleXMLElement); // bool(true)
Kiểm tra error log — bạn sẽ thấy các thông báo [XmlLoader] cho các input lỗi, và không có cảnh báo PHP nào ở bất kỳ đâu.
Một điều cần kiểm tra nếu lỗi vẫn tiếp diễn
Vẫn gặp lỗi dù chuỗi của bạn rõ ràng bắt đầu bằng <? Hãy tìm các ký tự vô hình. Một số API chèn null byte hoặc \r lạc trước XML. Dump các byte thô để tìm chúng:
echo bin2hex(substr($response, 0, 10));
// Kết quả mong đợi với UTF-8 XML: 3c3f786d6c ('<?xml')
// Tiền tố BOM trông như: efbbbf3c3f786d6c
Bất kỳ byte nào xuất hiện trước 3c (mã hex của <) chính là thủ phạm bạn cần tìm.

