Как исправить предупреждение “предварительный запрос не проходит контроль доступа: Нет заголовка ‘Access-Control-Allow-Origin'” в моем приложении на Express?

Вопрос или проблема

Я создаю сайт для магазина подержанных книг, который использует Aiven в качестве базы данных и Vercel для развертывания. Это довольно просто, так как это школьный проект, и он использует чистый JS. Я пытаюсь получить данные из нескольких API, но они совершенно не работают.

Вот мой server.js

const express = require('express');
const mysql = require('mysql2');
const path = require('path');
const cors = require('cors');
const fs = require('fs');

const fetch = (...args) =>
    import("node-fetch").then(({ default: fetch }) => fetch(...args));

const app = express();
const PORT = process.env.PORT || 3001;

app.use(express.static(path.join(__dirname, 'public')));

app.get("https://stackoverflow.com/", (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

const corsOptions = {
    origin: 'http://localhost:3001', // Ваш источник фронтенда
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
};

app.use(cors(corsOptions));
require('dotenv').config();

const connection = mysql.createConnection({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    port: process.env.DB_PORT,
    ssl: {
        rejectUnauthorized: false,
        ca: fs.readFileSync(process.env.SSL_CA),
    },
});

console.log('Подключение к базе данных:', {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    database: process.env.DB_NAME,
    port: process.env.DB_PORT,
});

connection.connect(err => {
    if (err) {
        console.error('Ошибка подключения к базе данных:', err);
        return;
    }
    console.log('Подключено к базе данных');
});

app.get('/test-db', (req, res) => {
    connection.query('SELECT 1', (err, results) => {
        if (err) {
            return res.status(500).json({ error: 'Ошибка подключения к базе данных' });
        }
        res.json({ message: 'База данных подключена', results });
    });
});

app.get('/books', (req, res) => {
    const query = 'SELECT * FROM books';

    connection.query(query, (err, results) => {
        if (err) {
            return res.status(500).json({ error: 'Ошибка получения книг' });
        }
        res.json(results);
    });
});

console.log('Хост базы данных:', process.env.DB_HOST);
console.log('Имя базы данных:', process.env.DB_NAME);

connection.connect(err => {
    if (err) {
        console.error('Ошибка подключения к базе данных:', err);
        return;
    }
    console.log('Подключено к базе данных');
});

app.listen(PORT, () => {
    console.log(`Сервер работает на http://localhost:${PORT}`);
});

console.log('Использование файла SSL CA:', process.env.SSL_CA);
console.log('Чтение CA:', fs.readFileSync(process.env.SSL_CA).toString());

app.use((req, res, next) => {
    console.log(`Входящий запрос: ${req.method} ${req.url}`);
    console.log('Заголовки запроса:', req.headers);
    next();
});

А вот HTML-файл, из которого извлекаются книги

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Инвентаризация</title>
    <link rel="stylesheet" href="istyle.css">
</head>
<body>
    <div id="navbar">
        <img src="images/BookbackICON.png" alt="">
        <label class="hamburger-menu">
            <input type="checkbox" />
        </label>
        <aside class="sidebar">
            <nav>
                <a href="index.html">Главная</a>
                <a href="inventory.html">Инвентаризация</a>
            </nav>
        </aside>
    </div>
    <section id="sec1">
        <span id="search">
            <input type="text" id="search-input" placeholder="Поиск книги по имени">
            <button id="search-button">Поиск</button>
        </span>
        <ul id="book-list"></ul>
    </section>

    <script>

        let books = [];  
        let uniqueBooks = [];  
        let duplicateCounts = {};

        function deduplicateBooks(data) {
    const uniqueBooksMap = {};
    duplicateCounts = {};

    data.forEach(book => {
        const isbnFull = book.id.split('-')[0]; // Используйте полный ISBN без обрезки последних 5 цифр
        const grade = book.grade; // Получите оценку

        const uniqueKey = `${isbnFull}-${grade}`;

        if (duplicateCounts[uniqueKey]) {
            duplicateCounts[uniqueKey]++;
        } else {
            duplicateCounts[uniqueKey] = 1;
            uniqueBooksMap[uniqueKey] = book; // Сохраните информацию об уникальной книге
        }
    });

    return Object.values(uniqueBooksMap);
}

function fetchBooks() {
fetch('https://bookback-i1o4juwqu-mohammed-aayan-pathans-projects.vercel.app/books', {
    method: 'GET', // Измените по необходимости
    headers: {
        'Content-Type': 'application/json', // Измените по необходимости
        // Любые другие заголовки, которые могут понадобиться
    },
    credentials: 'include', // Включите это, если вашем API требуются учетные данные
})
.then(response => {
    if (!response.ok) {
        throw new Error('Ответ сети был не в порядке: ' + response.statusText);
    }
    return response.json();
})
.then(data => {
    console.log('Полученные книги:', data);
})
.catch(error => {
    console.error('Ошибка извлечения:', error);
});
}

        function displayBooks(booksToDisplay) {
            const bookList = document.getElementById('book-list');
            bookList.innerHTML = ''; // Очистите текущий список

            const listItems = [];

            booksToDisplay.forEach(book => {
    const isbn = book.id.split('-')[0]; // Извлеките полную часть ISBN
    const li = document.createElement('li');
    li.style.cursor="pointer"; // Измените курсор на указатель для лучшего UX

    fetch(`https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`)
        .then(response => response.json())
        .then(bookData => {
            const img = document.createElement('img');
            img.style.borderRadius="4px";
            const title = book.name || 'Название недоступно';

            // Обработка данных Google Books
            if (bookData.items && bookData.items.length > 0) {
                const bookInfo = bookData.items[0].volumeInfo;
                const imageLinks = bookInfo.imageLinks;
                const coverImage = imageLinks?.large || imageLinks?.medium || imageLinks?.thumbnail || 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/No_image_available.svg/1200px-No_image_available.svg.png';
                img.src = coverImage;
            } else {
                img.src = `https://images-na.ssl-images-amazon.com/images/P/${isbn}.L.jpg`;
            }

            img.alt = title;
            img.style.maxWidth="200px";  // Настройте размер изображения по мере необходимости

            // Безопасное отображение цены (резервный вариант, если цена не является числом или отсутствует)
            let priceDisplay = 'Цена недоступна';
            if (book.price && !isNaN(book.price)) {
                priceDisplay = `AED ${parseFloat(book.price).toFixed(2)}`;
            }

            // Отображение информации о книге, названия, изображения и наличия
            li.appendChild(img);
            li.appendChild(document.createTextNode(title));
            li.appendChild(document.createElement('br'));
            li.appendChild(document.createTextNode(`Оценка: ${book.grade}`));
            li.appendChild(document.createElement('br'));
            li.appendChild(document.createTextNode(`${priceDisplay}`));

            // Исправьте логику отображения количества дубликатов
            const countKey = `${book.id.split('-')[0]}-${book.grade}`;  
            const countDisplay = `Кол-во на складе: ${duplicateCounts[countKey] || 1}`;
            li.appendChild(document.createElement('br'));
            li.appendChild(document.createTextNode(countDisplay));

            const isbnInfo = document.createElement('span');
            isbnInfo.textContent = `ISBN: ${book.id}`;
            isbnInfo.style.display = 'none'; // Скрыть изначально
            li.appendChild(isbnInfo); // Добавьте информацию об ISBN в элемент списка

            li.onclick = (event) => {
                event.stopPropagation(); // Предотвратить всплытие события клика

                listItems.forEach(item => {
                    item.style.filter="none"; // Убрать размытие
                    const isbnSpan = item.querySelector('span');
                    if (isbnSpan) {
                        isbnSpan.style.display = 'none'; // Скрыть ISBN
                    }
                });

                if (isbnInfo.style.display === 'none') {
                    isbnInfo.style.display = 'inline'; // Показать ISBN
                    li.style.height="auto"; // Развернуть выделенный элемент
                    li.style.filter="none"; // Убрать размытие у выделенного элемента
                } else {
                    isbnInfo.style.display = 'none'; // Скрыть ISBN
                }

                li.style.filter="none"; // Убедиться, что выделенный элемент не размыт
                listItems.forEach(item => {
                    if (item !== li) {
                        item.style.filter="blur(5px)"; // Размыть другие элементы
                    }
                });
            };

            listItems.push(li); // Добавьте текущий элемент в массив listItems

            bookList.appendChild(li);
        })
        .catch(error => {
            console.error('Ошибка при извлечении изображения книги из Google:', error);

            const img = document.createElement('img');
            img.src = `https://images-na.ssl-images-amazon.com/images/P/${isbn}.L.jpg`; // Резервное изображение Amazon
            img.alt="Нет изображения обложки доступно";
            img.style.maxWidth="200px";  // Настройте размер изображения по мере необходимости

            let priceDisplay = 'Цена недоступна';
            if (book.price && !isNaN(book.price)) {
                priceDisplay = `AED ${parseFloat(book.price).toFixed(2)}`;
            }

            li.appendChild(img);
            li.appendChild(document.createTextNode(` Название: ${book.name}, ISBN: ${book.id}, Оценка: ${book.grade}, ${priceDisplay}`));
            bookList.appendChild(li);
        });
});

            document.addEventListener('click', () => {
                listItems.forEach(item => {
                    item.style.height="260px"; // Сбросить высоту на авто
                    item.style.filter="none"; // Убрать размытие со всех элементов
                    const isbnSpan = item.querySelector('span');
                    if (isbnSpan) {
                        isbnSpan.style.display = 'none'; // Скрыть ISBN
                    }
                });
            });
        }

        function searchBooks() {
            const searchTerm = document.getElementById('search-input').value.toLowerCase();
            const filteredBooks = uniqueBooks.filter(book => book.name.toLowerCase().includes(searchTerm));
            displayBooks(filteredBooks);
        }

        document.getElementById('search-button').addEventListener('click', searchBooks);

        fetchBooks();
    </script>
</body>
</html>

Я пробовал делать всякие вещи, чтобы смягчить эту проблему, но это не сдвинется. Если вам нужно что-то еще, я предоставлю это как можно быстрее.

Ошибки, которые я получаю

Когда у нас есть предварительные запросы CORS, это означает, что у нас нет простого запроса!

Что такое простой запрос?
Некоторые запросы не вызывают предварительную проверку CORS. Их называют простыми запросами по устаревшей спецификации CORS, хотя спецификация Fetch (которая теперь определяет CORS) не использует этот термин.

Простой запрос – это тот, который удовлетворяет всем следующим условиям:

  1. GET
  2. HEAD
  3. POST

Единственные разрешенные комбинации типа/подтипа для типа медиа, указанного в заголовке Content-Type:

  1. application/x-www-form-urlencoded
  2. multipart/form-data
  3. text/plain

Исходя из вашего кода,

    headers: {
        'Content-Type': 'application/json', // Измените по необходимости
        // Любые другие заголовки, которые могут понадобиться
    },
    ```
ваш заголовок использует тип json, что вызывает предварительный запрос.

Ответ или решение

Согласно вашему описанию, вы сталкиваетесь с предупреждением preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header на вашем Express-приложении. Это связано с проблемой кросс-доменных запросов (CORS). Давайте разберем, как это можно решить.

Что такое CORS и предзапросы (preflight requests)?

CORS (Cross-Origin Resource Sharing) — это механизм, который позволяет контролировать доступ веб-страниц к ресурсам на других доменах. Когда ваш браузер делает запрос на другой домен, для некоторых типов запросов (не только GET, POST, HEAD, но и если они используют определенные заголовки, например Content-Type: application/json) сначала отправляется предзапрос (preflight request) с методом OPTIONS, чтобы проверить, разрешен ли такой запрос.

Как решить проблему CORS в вашем приложении

  1. Установка и использование модуля CORS:
    Вы уже используете пакет cors в вашем приложении, что хорошо. Однако убедитесь, что вы настраиваете заголовки CORS правильно.

    const corsOptions = {
       origin: 'http://localhost:3001', // Убедитесь, что здесь указывается правильный URL вашего фронтенда
       methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
       allowedHeaders: ['Content-Type', 'Authorization'],
    };
    
    app.use(cors(corsOptions));

    Если ваше приложение размещено на Vercel, возможно, вам нужно добавить домен, с которого вы делаете запросы, в опции origin.

  2. Настройка заголовков для предзапросов:
    В случае, если вы используете заголовок Content-Type: application/json, это вызывает предзапрос. Вы можете изменить это значение на одно из простых типов, которые не требуют предзапроса. Например, используйте:

    headers: {
       'Content-Type': 'application/x-www-form-urlencoded', // Или другой простой тип
    },

    Либо: просто удалите заголовок Content-Type, если это возможно в вашем приложении.

  3. Убедитесь, что сервер настроен правильно:
    Поместите app.use(cors(corsOptions)); в самом начале вашего файла server.js, перед всеми вашими маршрутами. Это гарантия, что все входящие запросы будут обрабатываться с учетом ваших настроек CORS.

  4. Проверьте консоль на наличие сообщений об ошибках:
    Если проблема сохраняется, посмотрите на консоль в браузере и на сервере, чтобы увидеть, нет ли дополнительных сообщений об ошибках.

Итоговые шаги

  • Убедитесь, что вы правильно настраиваете заголовки CORS.
  • Измените заголовок Content-Type, чтобы избежать предзапросов, если это возможно, или используйте необходимые заголовки при вызовах.
  • Проверьте консоли на наличие сообщений, которые могут помочь диагностировать проблему.

Вот пример того, как вы могли бы модифицировать свой fetch вызов:

fetch('https://bookback-i1o4juwqu-mohammed-aayan-pathans-projects.vercel.app/books', {
    method: 'GET',
    headers: {
        // 'Content-Type': 'application/json', // уберите, если это не обязательно
    },
    credentials: 'include',
})
.then(response => {
    // Ваш код
})
.catch(error => {
    console.error('Fetch error:', error);
});

Заключение

Настройка CORS может быть сложной задачей. Убедитесь, что вы тестируете ваше приложение и следите за тем, как обрабатываются запросы и ответы. Следуя этим шагам, вы сможете устранить проблему с CORS и гарантировать, что ваше приложение будет работать как задумано. Если у вас есть дополнительные вопросы, не стесняйтесь их задавать!

Оцените материал
Добавить комментарий

Капча загружается...