<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 17:18:45 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="319r7DNTflWTkP4MT_Ng4mNjg7JcjFfwRsJ1c58OcnMwjqDbwS3TNSXT2pRD_JCVo2quGFS7qVL7Iu8nzQmVHQ";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Rack | Ruby On Rails</title>
<meta name="description" content="Rack / Ruby On Rails: Изучаем Rack — интерфейс для разработки веб-приложений на Ruby">
<link rel="canonical" href="https://ru.hexlet.io/courses/rails-basics/lessons/rack/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Rack">
<meta property="og:title" content="Ruby On Rails">
<meta property="og:description" content="Rack / Ruby On Rails: Изучаем Rack — интерфейс для разработки веб-приложений на Ruby">
<meta property="og:url" content="https://ru.hexlet.io/courses/rails-basics/lessons/rack/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="zMbcCW52ffZXfIni6T9iLfPbzRrl7rK_vCb_OSXvEv0jFxc-nAjQluE_rXrlMJJaM9LgsO3ZTB0BxmVtd-j1kw" />
<script src="/vite/assets/inertia-INZxX8jp.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-6pOtQ3OW.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTExODcsInB1ciI6ImJsb2JfaWQifX0=--21b225d5b1f2f885f46e9ef4e79641fbea2f76dc/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Coding%20workshop-bro.webp"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T17:18:45.721Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"-RH3jK6OwrWH71rHIqkt2fcVEzbO9QP3bBzx7h0WCMQWwDy7XPBv1TGsfl8upt2uNxw-nMbC_VXR_Gu6TxHvqg","topics":[{"id":68643,"title":"Ссылка [Тестовые методы Rack](https://www.rubydoc.info/github/brynary/rack-test/master/Rack/Test/Methods)\nведет на не сущевствующую страницу","plain_title":"Ссылка (Тестовые методы Rack)[https://www.rubydoc.info/github/brynary/rack-test/master/Rack/Test/Methods] ведет на не сущевствующую страницу ","creator":{"public_name":"Андрей Шляпников","id":207990,"is_tutor":false},"comments":[{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":143783,"body":"Спасибо! Исправил ссылку","topic_id":68643}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":87591,"title":"Всё течёт, всё меняется.\n\n`Rack::Handler` перенесён в отдельный гем `rackup` ([changelog](https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md#binrackup-rackserver-rackhandlerand--racklobster-were-moved-to-a-separate-gem)).\n\nИ по дефолту `Rackup` содержит хендлеры только для `WEBrick` и `CGI` ([docs](https://msp-greg.github.io/rackup/Rackup/Handler.html)).\n\nИтоговый код:\n\n```ruby\nrequire 'rackup'\n\nclass MyApp\n def call(_env)\n [200, {'Content-Type' => 'text/html'}, [\"Hello\"]]\n end\nend\n\nRackup::Handler::WEBrick.run MyApp.new, :Port => 3000\n\n# Run with Puma\n# Rackup::Server.start app: MyApp, server: 'Puma', Port: 3000\n```\n\nХорошо бы экзамплы поправить.","plain_title":"Всё течёт, всё меняется. Rack::Handler перенесён в отдельный гем rackup (changelog (https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md#binrackup-rackserver-rackhandlerand--racklobster-were-moved-to-a-separate-gem)). И по дефолту Rackup содержит хендлеры только для WEBrick и CGI (docs (https://msp-greg.github.io/rackup/Rackup/Handler.html)). Итоговый код: require 'rackup' class MyApp def call(_env) [200, {'Content-Type' => 'text/html'}, [\"Hello\"]] end end Rackup::Handler::WEBrick.run MyApp.new, :Port => 3000 # Run with Puma # Rackup::Server.start app: MyApp, server: 'Puma', Port: 3000 Хорошо бы экзамплы поправить. ","creator":{"public_name":"Сергей К.","id":5174,"is_tutor":false},"comments":[{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":175452,"body":"Спасибо, за всем не уследишь :)","topic_id":87591}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":87148,"title":"Добрый день, не получается инициализировать репозиторйи командой ->\n\n> hexlet assignments init \\\n --hexlet-token=`//removed` \\\n --github-token=<token from github>\n\nТокен у себя в репозитории я создавал и как public_key и по классике в документации гитхаба, но дело, насколько я понимаю, по выводу ошибки внизу в токене hexlet.\n\n> Error: Github token must have public_repo permission\n at Object.checkToken (/usr/local/lib/node_modules/@hexlet/cli/src/utils/github.js:13:13)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async Object.module.exports [as handler] (/usr/local/lib/node_modules/@hexlet/cli/src/handlers/assignments/init.js:24:3)\n","plain_title":"Добрый день, не получается инициализировать репозиторйи командой -> hexlet assignments init \\ --hexlet-token=//removed \\ --github-token= Токен у себя в репозитории я создавал и как public_key и по классике в документации гитхаба, но дело, насколько я понимаю, по выводу ошибки внизу в токене hexlet. Error: Github token must have publicrepo permission at Object.checkToken (/usr/local/lib/nodemodules/@hexlet/cli/src/utils/github.js:13:13) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async Object.module.exports as handler (/usr/local/lib/node_modules/@hexlet/cli/src/handlers/assignments/init.js:24:3) ","creator":{"public_name":"Petr Che","id":147023,"is_tutor":false},"comments":[{"creator":{"public_name":"Petr Che","id":147023,"is_tutor":false},"id":174752,"body":"т.е. финальная команда выглядит так: \n\nhexlet assignments init\n--hexlet-token=`//removed`\n--github-token=`//removed`","topic_id":87148},{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":174747,"body":"Добрый день.\n\nНа всякий случай уточню, вы же подставляете созданный токен гитхаба в команду вместо `<token from github>`?","topic_id":87148},{"creator":{"public_name":"Petr Che","id":147023,"is_tutor":false},"id":174751,"body":"Добрый день еще раз, да подставляю, конечно :)","topic_id":87148},{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":174822,"body":"Проверьте, что вы создали токен с правильными правами:\nhttps://help.hexlet.io/ru/articles/308674-domasnie-zadaniya\n\nЯ не сразу увидел, но у вас в ошибке ругается на отсутствие public_repo permission.","topic_id":87148}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":87940,"title":"Не могу отправить домашку!\n\n➜ hexlet-assignments git:(main) ✗ pwd\n/Users/ivanz/projects/courses/hexlet/hexlet-assignments\n\n➜ hexlet-assignments git:(main) ✗ hexlet assignment submit\n\n\nhexlet assignment submit\n\nSubmit assignment (must be called from the assignment directory)\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n\nError: Submit command must be executed from assignment directory.\n at getAssignmentData (/Users/ivanz/.nvm/versions/node/v16.14.0/lib/node_modules/@hexlet/cli/src/utils/assignments.js:57:11)\n at Object.module.exports [as handler] (/Users/ivanz/.nvm/versions/node/v16.14.0/lib/node_modules/@hexlet/cli/src/handlers/assignment/submit.js:29:48)\n➜ hexlet-assignments git:(main) ✗\n\n\nвот что былло при ините\n\nConfig has been written to: /Users/ivanz/Hexlet/.config.json\nNothing changed. Skip committing.\nNothing to push. Skip pushing.\nPath to assignments: /Users/ivanz/projects/courses/hexlet/hexlet-assignments","plain_title":"Не могу отправить домашку! ➜ hexlet-assignments git:(main) ✗ pwd /Users/ivanz/projects/courses/hexlet/hexlet-assignments ➜ hexlet-assignments git:(main) ✗ hexlet assignment submit hexlet assignment submit Submit assignment (must be called from the assignment directory) Options: --version Show version number [boolean] --help Show help [boolean] Error: Submit command must be executed from assignment directory. at getAssignmentData (/Users/ivanz/.nvm/versions/node/v16.14.0/lib/node_modules/@hexlet/cli/src/utils/assignments.js:57:11) at Object.module.exports as handler (/Users/ivanz/.nvm/versions/node/v16.14.0/lib/node_modules/@hexlet/cli/src/handlers/assignment/submit.js:29:48) ➜ hexlet-assignments git:(main) ✗ вот что былло при ините Config has been written to: /Users/ivanz/Hexlet/.config.json Nothing changed. Skip committing. Nothing to push. Skip pushing. Path to assignments: /Users/ivanz/projects/courses/hexlet/hexlet-assignments ","creator":{"public_name":"Ivan Zabrodin","id":145566,"is_tutor":false},"comments":[{"creator":{"public_name":"Сергей К.","id":5174,"is_tutor":false},"id":175978,"body":"Аналогично. В любой директории вываливается ошибка:\n\n Error: Submit command must be executed from assignment directory.\n\nВ чём может быть проблема?\n","topic_id":87940},{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":176065,"body":"**Ivan Zabrodin**, выполнять команду `submit` нужно из директории конкретной домашней работы, то есть в вашем случае из `hexlet/hexlet-assignments/rack`","topic_id":87940}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":88067,"title":"При отправке ДЗ вываливается ошибка:\n\n```\n~/hexlet/hexlet-assignments/rails-basics-ru/rack (main) $ hexlet assignment submit\n\nhexlet assignment submit\n\nSubmit assignment (must be called from the assignment directory)\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n\nError: Submit command must be executed from assignment directory.\n at getAssignmentData (~/.asdf/installs/nodejs/19.9.0/lib/node_modules/@hexlet/cli/src/utils/assignments.js:57:11)\n at module.exports [as handler] (~/.asdf/installs/nodejs/19.9.0/lib/node_modules/@hexlet/cli/src/handlers/assignment/submit.js:29:48)\n```\n\nВерсия node v19.9.0. \nСодержимое конфига _.config.json_:\n\n```\n{\"githubToken\":\"...\",\"hexletToken\":\"...\",\"hexletDir\":\"~/Hexlet\",\"preferredLocale\":\"en\",\"assignments\":{\"githubUrl\":\"...\"}}\n```\n\nС чем может быть проблема? \nКак можно решить?","plain_title":"При отправке ДЗ вываливается ошибка: ~/hexlet/hexlet-assignments/rails-basics-ru/rack (main) $ hexlet assignment submit hexlet assignment submit Submit assignment (must be called from the assignment directory) Options: --version Show version number [boolean] --help Show help [boolean] Error: Submit command must be executed from assignment directory. at getAssignmentData (~/.asdf/installs/nodejs/19.9.0/lib/node_modules/@hexlet/cli/src/utils/assignments.js:57:11) at module.exports [as handler] (~/.asdf/installs/nodejs/19.9.0/lib/node_modules/@hexlet/cli/src/handlers/assignment/submit.js:29:48) Версия node v19.9.0. Содержимое конфига .config.json: {\"githubToken\":\"...\",\"hexletToken\":\"...\",\"hexletDir\":\"~/Hexlet\",\"preferredLocale\":\"en\",\"assignments\":{\"githubUrl\":\"...\"}} С чем может быть проблема? Как можно решить? ","creator":{"public_name":"Сергей К.","id":5174,"is_tutor":false},"comments":[{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":176129,"body":"Привет!\n\nОчень неявная ошибка, совсем не бросается в глаза. В конфиге `~/Hexlet`, а у тебя `~/hexlet`. Надо либо в конфиге поменять на маленькую, либо папку переименовать :)","topic_id":88067},{"creator":{"public_name":"Сергей К.","id":5174,"is_tutor":false},"id":176148,"body":"Сработало, спасибо!","topic_id":88067},{"creator":{"public_name":"Сергей К.","id":5174,"is_tutor":false},"id":176149,"body":"Опишу проблему.\n\nДиректория _hexlet_ была в домашнем каталоге изначально. Утилита создала в ней каталог для домашних заданий _hexlet-assignments_. Но в конфиг записала дефолтное значение _Hexlet_.","topic_id":88067},{"creator":{"public_name":"Сергей К.","id":5174,"is_tutor":false},"id":176075,"body":"Под node 18.16.0 та же ошибка.","topic_id":88067},{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":176160,"body":"Это особенности MacOS, ему вроде всё равно большая буква или маленькая, но на самом деле не всё равно ","topic_id":88067}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":84309,"title":"Все тесты выводят Rack::Lint::LintError: response is not an Array, but NilClass при этом даже с ответом который на сайте","plain_title":"Все тесты выводят Rack::Lint::LintError: response is not an Array, but NilClass при этом даже с ответом который на сайте ","creator":{"public_name":"Игорь Кот","id":593844,"is_tutor":false},"comments":[{"creator":{"public_name":"Игорь Кот","id":593844,"is_tutor":false},"id":170911,"body":"https://github.com/IregexI/hexlet-assignments/tree/main/rails-basics-ru/rack","topic_id":84309},{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":170968,"body":"Тесты запускают всё приложение со всеми мидлварами сразу. Посмотрите вот здесь: https://github.com/IregexI/hexlet-assignments/blob/main/rails-basics-ru/rack/lib/app.rb\n\nЕсли вы хотите проверить работу одной реализованной мидлвары, то закомментируйте ненужные. Либо реализуйте все :)","topic_id":84309},{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":170862,"body":"Можете прислать ссылку на ваше решение на вашем гитхабе? ","topic_id":84309},{"creator":{"public_name":"Александр Иноземцев","id":189031,"is_tutor":false},"id":179295,"body":"Тоже наткнулся на эту проблему и помогли только в чате, к сожалению сами примеры из видео не дают понимания как реализовать, то что просят в задании","topic_id":84309}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":89904,"title":"\"Практически все веб-фреймворке\"\nопечатка в задании: веб-фреймворкИ\n\nкстати, в exercism можно править в гитхабе даже описания заданий, может и в хекслете что-то такое прикрутить? с радостью бы пулреквесты иногда делал на такие штуки","plain_title":"\"Практически все веб-фреймворке\" опечатка в задании: веб-фреймворкИ кстати, в exercism можно править в гитхабе даже описания заданий, может и в хекслете что-то такое прикрутить? с радостью бы пулреквесты иногда делал на такие штуки ","creator":{"public_name":"Александр Иноземцев","id":189031,"is_tutor":false},"comments":[{"creator":{"public_name":"Aleksandr Litvinov","id":117182,"is_tutor":true},"id":178766,"body":"Такие опечатки можно отправлять нам нажатием Ctrl + Enter :)\n\nСам курс храниться в закрытом репозитории, поэтому доступ дать не можем. Но, например, курсы на code-basics открыты и там можно делать ПР для исправления опечаток и всякого такого.","topic_id":89904}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":90233,"title":"Не могу найти инструкцию по установке hexlet-cli, где описано как ее устанавливать?\nв итоге нашел в самом конце статьи\nnpm install -g @hexlet/cli, предлагаю это добавить в подготовке к дз, очень тяжело вспоминать где лежит утилита (разбесился пипец как)","plain_title":"Не могу найти инструкцию по установке hexlet-cli, где описано как ее устанавливать? в итоге нашел в самом конце статьи npm install -g @hexlet/cli, предлагаю это добавить в подготовке к дз, очень тяжело вспоминать где лежит утилита (разбесился пипец как) ","creator":{"public_name":"Александр Иноземцев","id":189031,"is_tutor":false},"comments":[{"creator":{"public_name":"Nikolai Gagarinov","id":104929,"is_tutor":true},"id":179287,"body":"Александр, добрый день.\n\nНа странице домашнего задания есть Слева ссылка со знаком вопроса. По ней открывается статья [Как выполнять домашние задания](https://help.hexlet.io/ru/articles/308674-domasnie-zadaniya).\n\nСпасибо, что обратили внимание на такую проблему. Попробуем улучшить наши инструкции. Если у вас будут в будущем сложности или вопросы - не стесняйтесь задавать вопрос наставнику или куратору.\n\n","topic_id":90233}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}},{"id":94150,"title":"Здраствуйте, не могу инициализировать репозиторий, появляется ошибка\n```\nhexlet assignments init\n\nInit assignments repository\n\nOptions:\n --version Show version number [boolean]\n --help Show help [boolean]\n --github-token Github Token [string] [required]\n --hexlet-token Hexlet token [string] [required]\n --hexlet-dir Path to Hexlet directory\n [string] [default: \"/home/maxa-honor-1/Hexlet\"]\n\nError: Invalid Hexlet token passed.\n at Object.initAssignments (/home/maxa-honor-1/.nvm/versions/node/v20.9.0/lib/node_modules/@hexlet/cli/src/utils/hexlet.js:62:11)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async module.exports [as handler] (/home/maxa-honor-1/.nvm/versions/node/v20.9.0/lib/node_modules/@hexlet/cli/src/handlers/assignments/init.js:47:31)\n\n```","plain_title":"Здраствуйте, не могу инициализировать репозиторий, появляется ошибка ``` hexlet assignments init Init assignments repository Options: --version Show version number [boolean] --help Show help [boolean] --github-token Github Token [string] [required] --hexlet-token Hexlet token [string] [required] --hexlet-dir Path to Hexlet directory [string] [default: \"/home/maxa-honor-1/Hexlet\"] Error: Invalid Hexlet token passed. at Object.initAssignments (/home/maxa-honor-1/.nvm/versions/node/v20.9.0/lib/nodemodules/@hexlet/cli/src/utils/hexlet.js:62:11) at process.processTicksAndRejections (node:internal/process/taskqueues:95:5) at async module.exports as handler (/home/maxa-honor-1/.nvm/versions/node/v20.9.0/lib/node_modules/@hexlet/cli/src/handlers/assignments/init.js:47:31) ","creator":{"public_name":"Владимир","id":507440,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":184471,"body":"Приветствую, Владимир! Вам нужно подтвердить вашу почту, тогда будет формироваться валидный Hexlet token","topic_id":94150}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Rack","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":4239,"slug":"rails_basics_rack_exercise","name":null,"state":"active","kind":"exercise","language":"ruby","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"Rack обеспечивает минимальный интерфейс между веб-серверами, поддерживающими Ruby и Ruby-фреймворки. Практически все веб-фреймворки в мире Ruby обеспечивают интерфейс Rack. При написании веб-приложений не всегда требуется использовать Ruby On Rails. Иногда требуется что-то намного проще, легче или даже быстрее.\n\nRack также поддерживает создание промежуточного слоя (middleware). По сути, они перехватывают запрос, что-то с ним делают и передают дальше. Это полезно для разноплановых задач, таких как логирование, аутентификация.\n\nВ этом задании создадим миддлвары для небольшого веб-приложения.\n\n### lib/app/router.rb\n\nРеализуйте класс Router, который содержит обработчики для 2 маршрутов: `/`, `/about`. При выполнении запросов по данным путям, возвращается строка и успешный код ответа (200). Для всех прочих маршрутов возвращается 404 код ответа и текст.\n\nТексты:\n\n* `/` - *Hello, World!*.\n* `/about` - *About page*.\n\n### lib/app/admin_policy.rb\n\nРеализуйте метод миддлвара. Который возвращает *403* статус ответа для всех запросов, путь которых начинается с */admin*. Возвращать тело ответа при этом не нужно.\n\n### lib/app/signature.rb\n\nРеализуйте метод миддлвара, который добавляет в тело ответа хеш, зашифрованный по алгоритму семейства *SHA-256*\n\n```bash\ncurl http://localhost:3000\n# Hello, World!\n# dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f\n```\n\n## lib/app.rb\n\nПодключите мидлвары в приложение в указанном порядке:\n\n1. `AdminPolicy`.\n2. `Signature`.\n\n### Подсказки\n\n* [Response Body](https://github.com/rack/rack/blob/main/SPEC.rdoc#label-The+Body)\n* [Digest::SHA2](https://ruby-doc.org/stdlib-2.4.0/libdoc/digest/rdoc/Digest/SHA2.html)\n","prepared_readme":"Rack обеспечивает минимальный интерфейс между веб-серверами, поддерживающими Ruby и Ruby-фреймворки. Практически все веб-фреймворки в мире Ruby обеспечивают интерфейс Rack. При написании веб-приложений не всегда требуется использовать Ruby On Rails. Иногда требуется что-то намного проще, легче или даже быстрее.\n\nRack также поддерживает создание промежуточного слоя (middleware). По сути, они перехватывают запрос, что-то с ним делают и передают дальше. Это полезно для разноплановых задач, таких как логирование, аутентификация.\n\nВ этом задании создадим миддлвары для небольшого веб-приложения.\n\n### lib/app/router.rb\n\nРеализуйте класс Router, который содержит обработчики для 2 маршрутов: `/`, `/about`. При выполнении запросов по данным путям, возвращается строка и успешный код ответа (200). Для всех прочих маршрутов возвращается 404 код ответа и текст.\n\nТексты:\n\n* `/` - *Hello, World!*.\n* `/about` - *About page*.\n\n### lib/app/admin_policy.rb\n\nРеализуйте метод миддлвара. Который возвращает *403* статус ответа для всех запросов, путь которых начинается с */admin*. Возвращать тело ответа при этом не нужно.\n\n### lib/app/signature.rb\n\nРеализуйте метод миддлвара, который добавляет в тело ответа хеш, зашифрованный по алгоритму семейства *SHA-256*\n\n```bash\ncurl http://localhost:3000\n# Hello, World!\n# dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f\n```\n\n## lib/app.rb\n\nПодключите мидлвары в приложение в указанном порядке:\n\n1. `AdminPolicy`.\n2. `Signature`.\n\n### Подсказки\n\n* [Response Body](https://github.com/rack/rack/blob/main/SPEC.rdoc#label-The+Body)\n* [Digest::SHA2](https://ruby-doc.org/stdlib-2.4.0/libdoc/digest/rdoc/Digest/SHA2.html)\n","has_solution":true,"entity_name":"Rack"},"units":[{"id":4552,"name":"theory","url":"/courses/rails-basics/lessons/rack/theory_unit"},{"id":13260,"name":"quiz","url":"/courses/rails-basics/lessons/rack/quiz_unit"},{"id":14493,"name":"exercise","url":"/courses/rails-basics/lessons/rack/exercise_unit"}],"links":[{"id":424123,"name":"Курс Протокол HTTP","url":"https://ru.hexlet.io/programs/http-api\n"},{"id":424124,"name":"Тестовые методы Rack","url":"https://www.rubydoc.info/github/brynary/rack-test/Rack/Test/Methods\n"},{"id":424125,"name":"Репозиторий Rack","url":"https://github.com/rack/rack\n"},{"id":424126,"name":"Sinatra","url":"https://sinatrarb.com/\n"},{"id":424127,"name":"Пример Hello, World! на Sinatra","url":"https://github.com/hexlet-components/sinatra-app\n"}],"ordered_units":[{"id":4552,"name":"theory","url":"/courses/rails-basics/lessons/rack/theory_unit"},{"id":13260,"name":"quiz","url":"/courses/rails-basics/lessons/rack/quiz_unit"},{"id":14493,"name":"exercise","url":"/courses/rails-basics/lessons/rack/exercise_unit"}],"id":2022,"slug":"rack","state":"approved","name":"Rack","course_order":150,"goal":"Изучаем Rack — интерфейс для разработки веб-приложений на Ruby","self_study":null,"theory_video_provider":"vimeo","theory_video_uid":"568189756","theory":"\nRack – это интерфейс для взаимодействия веб-сервера с HTTP-запросами. У него есть несколько функций:\n\n* Стандарт интерфейса веб-сервера. Rack определяет, как серверы обрабатывают запросы и взаимодействуют друг с другом. Позволяет использовать различные серверы для запуска приложений\n\n* Каркас для Middlewares. Rack работает как каркас для middleware, которые обрабатывают запросы по конвейерному принципу.\n\n* Библиотека. Rack содержит вспомогательные функции для более быстрой разработки. Используется в таких фреймворках, как Sinatra и Rails.\n\nПонимание Rack важно для разработки на Sinatra и Rails, поскольку они подчиняются его стандартам.\n\n## Приложение Rack\n\nЧтобы запустить Rack, необходимо импортировать библиотеку, вызвать хендлер и применить метод `run()`, передавая объект, который содержит метод `call()`. Приложение запускается командой `ruby app.rb`\n\n```ruby\n# app.rb\nrequire 'rackup'\n\nclass MyApp\n def call(_env)\n [200, {'Content-Type' => 'text/html'}, [\"Hello\"]]\n end\nend\n\nRackup::Handler::WEBrick.run MyApp.new, :Port => 3000\n```\n\nМетод `call()` должен вернуть массив из трех элементов:\n\n* Статус ответа.\n* Хедеры в виде хэша.\n* Тело ответа. [Тело ответа](https://github.com/rack/rack/blob/main/SPEC.rdoc#label-The+Body) обычно представляет собой массив строк\n\nРазработчики Rack также создали консольную утилиту под названием *rackup*. Эта утилита ищет файл с именем *config.ru* в текущей папке и запускает сервер на порту 9292. Если в *config.ru* указано приложение, то при отправке GET-запроса на порт 9292 сервер вернет тело ответа, указанное в качестве третьего элемента в конфигурации приложения.\n\n```ruby\n# config.ru\nclass MyApp\n def call(_env)\n [200, {'Content-Type' => 'text/html'}, [\"Hello\"]]\n end\nend\n\nrun MyApp.new\n```\n\nЗапуск может выполняться одной из команд в зависимости от используемого сервера\n\n```bash\n# автоматически ищется файл config.ru\nrackup\nrackup -s thin\nthin start\npuma\nunicorn\npassenger start\n```\n\nПроверяем работу приложения\n\n```bash\n# Запустили сервер\nrackup\n\n# В другом терминале выполнили запрос\ncurl -X GET localhost:9292\n# => Hello!\n```\n\n## Middlewares\n\nMiddleware – это фильтры запросов, которые обрабатывают информацию о запросе и передают её следующему компоненту.\n\nПрименение middleware:\n\n* Авторизация\n\n Middleware может управлять доступом, включая встроенные решения для basic-auth\n\n* Мониторинг\n\n Можно отслеживать количество запросов и их время выполнения\n\n* Логирование\n\n Подходит для записи работы приложения, особенно на уровне системы.\n\n* Сериализация\n\n Поддерживает передачу данных, включая динамические переменные.\n\n* Роутинг\n\n Доступ к параметрам запроса позволяет определять, как следует обрабатывать запрос.\n\n* Бизнес-логика\n\n Может реализовываться через вызов сервисных объектов в middleware.\n\nЗапрос поступает в приложение и передается через цепочку middleware, каждая из которых обрабатывает определенную логику. Ответ формируется последней middleware.\n\nКаждый компонент middleware должен возвращать три элемента: статус, хедеры и тело ответа.\n\nНиже пример приложения, содержащее миддлвар\n\n```ruby\n# config.ru\nclass MyMiddleware\n def initialize(app1)\n @app1 = app1\n end\n\n def call(env)\n puts 'middleware_before'\n # env содержит запрос\n status, headers, body = @app1.call(env)\n puts 'middleware_after'\n request = Rack::Request.new(env)\n if request.path == '/'\n case request.request_method\n when 'GET'\n [status, headers, body]\n when 'POST'\n [201, headers.merge({'x-created' => 'True'}), ['Item was successfully created']]\n end\n else\n [404, {}, [\"Not Found\"]]\n end\n end\nend\n\nclass App\n def call(env)\n puts 'app_run'\n [200, {}, [\"success\"]]\n end\nend\n\n# Добавляется миддлвар\nuse MyMiddleware\n# Запуск приложения\nrun App.new\n```\n\nПример приложения, которое обрабатывает POST запрос:\n\n```ruby\n\nrequire 'json'\n\nclass MyMiddleware\n def initialize(app1)\n @app1 = app1\n end\n\n def call(env)\n status, headers, body = @app1.call(env)\n request = Rack::Request.new(env)\n\n body = {\n path: request.path,\n verb: request.request_method,\n ip: request.ip,\n cookies: request.cookies,\n params: request.params,\n body: JSON.parse(request.body.read)\n }\n\n [200, {}, [body.to_json]]\n end\nend\n\nclass App\n def call(env)\n end\nend\n\nuse MyMiddleware\nrun App.new\n```\n\nЗапуск и выполнение запроса:\n\n```bash\nrackup\ncurl -x POST localhost:9292/users?sort=desc -d \"{\\\"login\\\":\\\"admin\\\",\\\"password\\\":\\\"password\\\"}\"\n```\n\nПример Middleware, который обрабатывает предыдущий ответ от приложения и добавляет к телу новую информацию. В этом примере добавляется к телу ответа добавляется текущее время\n\n```ruby\nclass TimeStamp\n def initialize(app)\n @app = app\n end\n\n def call(env)\n # Вызываем приложение и получаем его ответ\n prev_response = @app.call(env)\n status, headers, prev_body = prev_response\n\n # Если статус не 200, возвращаем предыдущий ответ без изменений,\n # чтобы не обрабатывать не успешные ответы\n return prev_response if status != 200\n\n # Получаем текущее время в формате строки\n current_time = Time.now.strftime(\"%Y-%m-%d %H:%M:%S\")\n\n # Добавляем текущее время к предыдущему телу ответа\n next_body = prev_body.push('</br>', \"Текущее время: #{current_time}\")\n\n # Возвращаем новый ответ с добавленным временем\n [status, headers, next_body]\n end\nend\n\n```\n\nВ этом примере миддлвара `Timestamp` принимает на вход приложение Rack и перехватывает его ответ. Если статус ответа равен 200 (успешный ответ), миддлвара добавляет к телу ответа текущую дату и время в виде строки.\n\nПриложение с базовой авторизацией\n\n```ruby\nuse Rack::Auth::Basic do |username, password|\n username == 'admin' && password == 'password'\nend\n\nclass App\n def call(env)\n puts env[\"HTTP_AUTHORIZATION\"]\n [200, {'Content-Type' => 'text/html'}, [\"You have been successfully logged in.\"]]\n end\nend\nrun App.new\n```\n\nЗапуск и запрос с авторизацией\n\n```bash\nrackup\ncurl -u admin:password -i http://localhost:9292\n```\n\n## Тестирование Rack-приложений\n\nЗапуск выполняется командой `ruby test.rb`\n\n```ruby\n# test.rb\nrequire 'minitest/autorun'\nrequire 'rack/test'\n\nclass MyApp\n def call(env)\n [200, {'X-success' => true}, [\"Success response\"]]\n end\nend\n\ndescribe \"MyApp\" do\n include Rack::Test::Methods\n\n def app\n MyApp.new\n end\n\n it 'check response status' do\n get '/'\n assert last_response.ok?\n end\n\n it 'check response headers' do\n get '/'\n\n assert_equal last_response.headers, {'X-success' => true}\n end\n\n it 'check response body' do\n get '/'\n assert_equal last_response.body, \"Success response\"\n end\nend\n\n#\n# https://www.rubydoc.info/github/brynary/rack-test/Rack/Test/Methods\n# https://devhints.io/rack-test\n```\n\n## Мидлвары Rack\n\nRack предоставляет различные готовые middleware для улучшения функционала, вот некоторые из них:\n\nСам Rack поставляется со следующим промежуточным программным обеспечением:\n\n* `Rack::Files` для раздачи статических файлов.\n* `Rack::Events` для создания удобных хуков при получении запроса и отправке ответа.\n* `Rack::Head` для возврата пустого тела для HEAD-запросов.\n* `Rack::Lock` для сериализации запросов с помощью мьютекса.\n* `Rack::Reloader` для перезагрузки приложения, если были изменены файлы.\n* `Rack::Runtime` для включения в заголовок ответа времени, затраченного на обработку запроса.\n* `Rack::ShowException` для перехвата необработанных исключений и представления их в удобном виде.\n* `Rack::MethodOverride` для изменения метода запроса на основе переданного параметра.\n\n## Хелперы Rack\n\nRack предоставляет множество хелперов:\n\n* `Rack::Request` - обеспечивает разбор строки запроса и работу с несколькими частями.\n* `Rack::Response` - для удобной генерации HTTP-ответов и обработки cookie.\n* `Rack::MockRequest` и `Rack::MockResponse` для эффективного и быстрого тестирования Rack-приложений без реальных HTTP-сессий.\n* `Rack::Directory` - для раздачи файлов в директории.\n* `Rack::MediaType` - для разбора заголовков типа содержимого.\n* `Rack::Mime` - для определения типа содержимого на основе расширения файла.\n\n## Sinatra\n\nSinatra — это легковесный веб-фреймворк, построенный на основе Rack. Он предлагает простой и элегантный способ создания веб-приложений, предоставляя разработчикам возможность быстро разрабатывать RESTful API и небольшие веб-приложения.\n\nSinatra имеет минималистичный синтаксис, что делает его идеальным выбором для небольших проектов и прототипов. Фреймворк позволяет добавлять только необходимые компоненты, что позволяет создавать приложения, максимально соответствующие вашим требованиям. Также он поддерживает использование middleware, что позволяет комбинировать различные библиотеки и улучшать функциональность вашего приложения.\n\n### Пример простого приложения на Sinatra\n\nСоздаем директорию проекта:\n\n```bash\nmkdir sinatra-app\ncd sinatra-app\nbundle init\n```\n\nДобавляем зависимости в _Gemfile_:\n\n```ruby\nsource 'https://rubygems.org'\n\ngem 'puma'\ngem 'rackup'\ngem 'sinatra'\n```\n\nСоздаем _app.rb_ - точку входа в наше приложение:\n\n```ruby\nrequire 'sinatra'\n\nget '/' do\n 'Hello, world!'\nend\n\nget '/hello/:name' do\n \"Hello, #{params['name']}!\"\nend\n```\n\nЗапускаем:\n\n```text\nruby app.rb\n== Sinatra (v4.1.1) has taken the stage on 4567 for development with backup from Puma\nPuma starting in single mode...\n* Puma version: 6.5.0 (\"Sky's Version\")\n* Ruby version: ruby 3.3.4 (2024-07-09 revision be1089c8ec) [x86_64-linux]\n* Min threads: 0\n* Max threads: 5\n* Environment: development\n* PID: 138933\n* Listening on http://127.0.0.1:4567\n* Listening on http://[::1]:4567\nUse Ctrl-C to stop\n```\n\nНаше приложение будет доступно по адресу *http://localhost:4567*, и на странице будет выведена строка *Hello, World!*\n\n## Заключение\n\nВ этом уроке мы познакомились с основами Rack, его компонентами и концепцией middleware. Мы также рассмотрели Sinatra как легковесный фреймворк, построенный на основе Rack.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":7193,"name":"theory","url":"/courses/rails-basics/lessons/intro/theory_unit"}],"links":[{"id":424121,"name":"Rails Guides","url":"https://guides.rubyonrails.org/index.html\n"}],"ordered_units":[{"id":7193,"name":"theory","url":"/courses/rails-basics/lessons/intro/theory_unit"}],"id":3211,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с целями и задачами курса","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Этот курс посвящен Ruby on Rails – самому популярному фреймворку на Ruby. В этом курсе мы будем создавать простые веб-приложения и рассмотрим следующие темы:\n\n* Создание CRUD, валидация данных\n* ORM, создание сущностей и связи\n* Конфигурация приложения\n* Вложенные ресурсы\n* Тестирование и линтинг\n* i18n и локализация\n* Написание middlewares\n\nДля полноценного погружения рекомендуем выполнять все домашние задания и выкатывать результат на сервисы, подобные [Render](https://render.com).\n"},"id":234,"slug":"rails-basics","challenges_count":0,"name":"Ruby On Rails","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"В этом курсе вы изучите основы работы с фреймоворком Ruby on Rails. Вы узнаете о роутинге, контроллерах, шаблонизаторах, моделях и связях между ними. В итоге научитесь создавать полноценные сайты с CRUD действиями, писать тесты на них, работать с базой данных через ActiveRecord и миграции. Знания из этого курса помогут создать свой сайт и сделать его доступным в интернете.","kind":"basic","updated_at":"2026-01-20T11:46:14.961Z","language":"ruby","duration_cache":61080,"skills":["Создавать сайты на Rails, покрывать их автоматическими тестами и выкладывать на PaaS-сервисы","Основным концепциям фреймворка, таким как контроллеры, роутинг, модели, шаблоны и тесты.","Автоматизировать большинство рутинных задач с помощью генераторов, автоматических форм и интеграций фронтенда.","Использовать repl и эффективно отлаживать приложения","Основным принципам построения безопасных приложений."],"keywords":["ruby","rails","orm","rest api","job workers","архитертура","rake"],"lessons_count":16,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NzM3MiwicHVyIjoiYmxvYl9pZCJ9fQ==--3b3f159f182adb8eb21f75671f2839f8c4374db6/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[{"stack":{"id":475,"slug":"rails","title":"Ruby on Rails","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"not_finished","order":null,"duration_in_months":4},"id":632,"slug":"rails","title":"Разработка на Ruby on Rails","subtitle":"Изучите Ruby, Rails и проектирование REST API","subtitle_for_lists":"Изучите Ruby, Rails и проектирование REST API","locale":"ru","current":true,"duration_in_months_text":"4 месяца","stack_slug":"rails","price_text":"от 3 900 ₽","duration_text":"4 месяца","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTExODcsInB1ciI6ImJsb2JfaWQifX0=--21b225d5b1f2f885f46e9ef4e79641fbea2f76dc/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Coding%20workshop-bro.webp"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/rails-basics/lessons/rack/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Ruby On Rails</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Rack</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Rack","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Ruby On Rails"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Rack – это интерфейс для взаимодействия веб-сервера с HTTP-запросами. У него есть несколько функций:</p>
<ul>
<li>
<p>Стандарт интерфейса веб-сервера. Rack определяет, как серверы обрабатывают запросы и взаимодействуют друг с другом. Позволяет использовать различные серверы для запуска приложений</p>
</li>
<li>
<p>Каркас для Middlewares. Rack работает как каркас для middleware, которые обрабатывают запросы по конвейерному принципу.</p>
</li>
<li>
<p>Библиотека. Rack содержит вспомогательные функции для более быстрой разработки. Используется в таких фреймворках, как Sinatra и Rails.</p>
</li>
</ul>
<p>Понимание Rack важно для разработки на Sinatra и Rails, поскольку они подчиняются его стандартам.</p>
<h2 id="heading-2-1">Приложение Rack</h2>
<p>Чтобы запустить Rack, необходимо импортировать библиотеку, вызвать хендлер и применить метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">run()</code>, передавая объект, который содержит метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">call()</code>. Приложение запускается командой <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ruby app.rb</code></p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># app.rb
require 'rackup'
class MyApp
def call(_env)
[200, {'Content-Type' => 'text/html'}, ["Hello"]]
end
end
Rackup::Handler::WEBrick.run MyApp.new, :Port => 3000</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">call()</code> должен вернуть массив из трех элементов:</p>
<ul>
<li>Статус ответа.</li>
<li>Хедеры в виде хэша.</li>
<li>Тело ответа. <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://github.com/rack/rack/blob/main/SPEC.rdoc#label-The+Body" rel="noopener noreferrer" target="_blank">Тело ответа</a> обычно представляет собой массив строк</li>
</ul>
<p>Разработчики Rack также создали консольную утилиту под названием <em>rackup</em>. Эта утилита ищет файл с именем <em>config.ru</em> в текущей папке и запускает сервер на порту 9292. Если в <em>config.ru</em> указано приложение, то при отправке GET-запроса на порт 9292 сервер вернет тело ответа, указанное в качестве третьего элемента в конфигурации приложения.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># config.ru
class MyApp
def call(_env)
[200, {'Content-Type' => 'text/html'}, ["Hello"]]
end
end
run MyApp.new</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Запуск может выполняться одной из команд в зависимости от используемого сервера</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># автоматически ищется файл config.ru
rackup
rackup -s thin
thin start
puma
unicorn
passenger start</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Проверяем работу приложения</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># Запустили сервер
rackup
# В другом терминале выполнили запрос
curl -X GET localhost:9292
# => Hello!</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h2 id="heading-2-2">Middlewares</h2>
<p>Middleware – это фильтры запросов, которые обрабатывают информацию о запросе и передают её следующему компоненту.</p>
<p>Применение middleware:</p>
<ul>
<li>
<p>Авторизация</p>
<p>Middleware может управлять доступом, включая встроенные решения для basic-auth</p>
</li>
<li>
<p>Мониторинг</p>
<p>Можно отслеживать количество запросов и их время выполнения</p>
</li>
<li>
<p>Логирование</p>
<p>Подходит для записи работы приложения, особенно на уровне системы.</p>
</li>
<li>
<p>Сериализация</p>
<p>Поддерживает передачу данных, включая динамические переменные.</p>
</li>
<li>
<p>Роутинг</p>
<p>Доступ к параметрам запроса позволяет определять, как следует обрабатывать запрос.</p>
</li>
<li>
<p>Бизнес-логика</p>
<p>Может реализовываться через вызов сервисных объектов в middleware.</p>
</li>
</ul>
<p>Запрос поступает в приложение и передается через цепочку middleware, каждая из которых обрабатывает определенную логику. Ответ формируется последней middleware.</p>
<p>Каждый компонент middleware должен возвращать три элемента: статус, хедеры и тело ответа.</p>
<p>Ниже пример приложения, содержащее миддлвар</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># config.ru
class MyMiddleware
def initialize(app1)
@app1 = app1
end
def call(env)
puts 'middleware_before'
# env содержит запрос
status, headers, body = @app1.call(env)
puts 'middleware_after'
request = Rack::Request.new(env)
if request.path == '/'
case request.request_method
when 'GET'
[status, headers, body]
when 'POST'
[201, headers.merge({'x-created' => 'True'}), ['Item was successfully created']]
end
else
[404, {}, ["Not Found"]]
end
end
end
class App
def call(env)
puts 'app_run'
[200, {}, ["success"]]
end
end
# Добавляется миддлвар
use MyMiddleware
# Запуск приложения
run App.new</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Пример приложения, которое обрабатывает POST запрос:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">require 'json'
class MyMiddleware
def initialize(app1)
@app1 = app1
end
def call(env)
status, headers, body = @app1.call(env)
request = Rack::Request.new(env)
body = {
path: request.path,
verb: request.request_method,
ip: request.ip,
cookies: request.cookies,
params: request.params,
body: JSON.parse(request.body.read)
}
[200, {}, [body.to_json]]
end
end
class App
def call(env)
end
end
use MyMiddleware
run App.new</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Запуск и выполнение запроса:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">rackup
curl -x POST localhost:9292/users?sort=desc -d "{\"login\":\"admin\",\"password\":\"password\"}"</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Пример Middleware, который обрабатывает предыдущий ответ от приложения и добавляет к телу новую информацию. В этом примере добавляется к телу ответа добавляется текущее время</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class TimeStamp
def initialize(app)
@app = app
end
def call(env)
# Вызываем приложение и получаем его ответ
prev_response = @app.call(env)
status, headers, prev_body = prev_response
# Если статус не 200, возвращаем предыдущий ответ без изменений,
# чтобы не обрабатывать не успешные ответы
return prev_response if status != 200
# Получаем текущее время в формате строки
current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
# Добавляем текущее время к предыдущему телу ответа
next_body = prev_body.push('</br>', "Текущее время: #{current_time}")
# Возвращаем новый ответ с добавленным временем
[status, headers, next_body]
end
end</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В этом примере миддлвара <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Timestamp</code> принимает на вход приложение Rack и перехватывает его ответ. Если статус ответа равен 200 (успешный ответ), миддлвара добавляет к телу ответа текущую дату и время в виде строки.</p>
<p>Приложение с базовой авторизацией</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">use Rack::Auth::Basic do |username, password|
username == 'admin' && password == 'password'
end
class App
def call(env)
puts env["HTTP_AUTHORIZATION"]
[200, {'Content-Type' => 'text/html'}, ["You have been successfully logged in."]]
end
end
run App.new</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Запуск и запрос с авторизацией</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">rackup
curl -u admin:password -i http://localhost:9292</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h2 id="heading-2-3">Тестирование Rack-приложений</h2>
<p>Запуск выполняется командой <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ruby test.rb</code></p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># test.rb
require 'minitest/autorun'
require 'rack/test'
class MyApp
def call(env)
[200, {'X-success' => true}, ["Success response"]]
end
end
describe "MyApp" do
include Rack::Test::Methods
def app
MyApp.new
end
it 'check response status' do
get '/'
assert last_response.ok?
end
it 'check response headers' do
get '/'
assert_equal last_response.headers, {'X-success' => true}
end
it 'check response body' do
get '/'
assert_equal last_response.body, "Success response"
end
end
#
# https://www.rubydoc.info/github/brynary/rack-test/Rack/Test/Methods
# https://devhints.io/rack-test</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h2 id="heading-2-4">Мидлвары Rack</h2>
<p>Rack предоставляет различные готовые middleware для улучшения функционала, вот некоторые из них:</p>
<p>Сам Rack поставляется со следующим промежуточным программным обеспечением:</p>
<ul>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Files</code> для раздачи статических файлов.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Events</code> для создания удобных хуков при получении запроса и отправке ответа.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Head</code> для возврата пустого тела для HEAD-запросов.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Lock</code> для сериализации запросов с помощью мьютекса.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Reloader</code> для перезагрузки приложения, если были изменены файлы.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Runtime</code> для включения в заголовок ответа времени, затраченного на обработку запроса.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::ShowException</code> для перехвата необработанных исключений и представления их в удобном виде.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::MethodOverride</code> для изменения метода запроса на основе переданного параметра.</li>
</ul>
<h2 id="heading-2-5">Хелперы Rack</h2>
<p>Rack предоставляет множество хелперов:</p>
<ul>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Request</code> - обеспечивает разбор строки запроса и работу с несколькими частями.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Response</code> - для удобной генерации HTTP-ответов и обработки cookie.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::MockRequest</code> и <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::MockResponse</code> для эффективного и быстрого тестирования Rack-приложений без реальных HTTP-сессий.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Directory</code> - для раздачи файлов в директории.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::MediaType</code> - для разбора заголовков типа содержимого.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Rack::Mime</code> - для определения типа содержимого на основе расширения файла.</li>
</ul>
<h2 id="heading-2-6">Sinatra</h2>
<p>Sinatra — это легковесный веб-фреймворк, построенный на основе Rack. Он предлагает простой и элегантный способ создания веб-приложений, предоставляя разработчикам возможность быстро разрабатывать RESTful API и небольшие веб-приложения.</p>
<p>Sinatra имеет минималистичный синтаксис, что делает его идеальным выбором для небольших проектов и прототипов. Фреймворк позволяет добавлять только необходимые компоненты, что позволяет создавать приложения, максимально соответствующие вашим требованиям. Также он поддерживает использование middleware, что позволяет комбинировать различные библиотеки и улучшать функциональность вашего приложения.</p>
<h3 id="heading-3-7">Пример простого приложения на Sinatra</h3>
<p>Создаем директорию проекта:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">mkdir sinatra-app
cd sinatra-app
bundle init</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Добавляем зависимости в <em>Gemfile</em>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">source 'https://rubygems.org'
gem 'puma'
gem 'rackup'
gem 'sinatra'</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Создаем <em>app.rb</em> - точку входа в наше приложение:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">require 'sinatra'
get '/' do
'Hello, world!'
end
get '/hello/:name' do
"Hello, #{params['name']}!"
end</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Запускаем:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">ruby app.rb
== Sinatra (v4.1.1) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.5.0 ("Sky's Version")
* Ruby version: ruby 3.3.4 (2024-07-09 revision be1089c8ec) [x86_64-linux]
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 138933
* Listening on http://127.0.0.1:4567
* Listening on http://[::1]:4567
Use Ctrl-C to stop</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Наше приложение будет доступно по адресу <em><a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="http://localhost:4567" rel="noopener noreferrer" target="_blank">http://localhost:4567</a></em>, и на странице будет выведена строка <em>Hello, World!</em></p>
<h2 id="heading-2-8">Заключение</h2>
<p>В этом уроке мы познакомились с основами Rack, его компонентами и концепцией middleware. Мы также рассмотрели Sinatra как легковесный фреймворк, построенный на основе Rack.</p></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/rails?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">4 месяца</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Разработка на Ruby on Rails</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите Ruby, Rails и проектирование REST API</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTExODcsInB1ciI6ImJsb2JfaWQifX0=--21b225d5b1f2f885f46e9ef4e79641fbea2f76dc/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Coding%20workshop-bro.webp" alt="Разработка на Ruby on Rails" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/rails-basics/lessons/rack/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 16</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/rails-basics/lessons/rack/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-CdBlNCiQ.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>