Please help me! I'm building a simple stock search app using Flask (backend) and plain HTML/JS (frontend). When the user enters a ticker, the app:
Send a GET request to my Flask backend (/search)
Display the stock data returned from the Tiingo API
After successful display, send a POST request to /add_to_history to store the ticker in a SQLite database
The GET request works correctly — the data displays as expected in my table.
The problem: As soon as the POST request (/add_to_history) returns a 200 response, the table that was displaying the fetched stock data immediately disappears from the page. There are no errors in the console, and both network requests return status 200. The UI simply hides the content right after the POST call finishes. Here is my index.html code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Stock App</title>
<script>
window.stockData = {
"companyOutLook": null,
"stockSummary": null
}
function onBtnClick(event) {
event.preventDefault();
const inputField = document.getElementById('stockTicker');
const tickerSymbol = inputField.value;
fetch(`http://127.0.0.1:5000/search?ticker=${encodeURIComponent(tickerSymbol)}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Data received:', data);
// Check if response contains an error
if (data.error) {
hideTabsAndTable();
showErrorMessage();
return;
}
// Store data for later use
if (Array.isArray(data) && data.length > 0) {
window.stockData.companyOutLook = data[0];
if (data.length > 1) {
window.stockData.stockSummary = data[1];
}
else {
window.stockData.stockSummary = null;
}
// Show tabs and table when data is available
hideErrorMessage();
showTabsAndTable();
showCompanyOutlook();
// add to sqlite history
fetch(`http://127.0.0.1:5000/add_to_history`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ticker: tickerSymbol}),
})
.then(response => {
console.log('Response:', response);
})
.catch(error => {
console.error('Error:', error);
});
}
else {
window.stockData.companyOutLook = null;
window.stockData.stockSummary = null;
hideTabsAndTable();
showErrorMessage();
}
})
.catch(error => {
hideTabsAndTable();
showErrorMessage();
});
}
function showTabsAndTable() {
const tabs = document.querySelector('.tabs');
const table = document.querySelector('.content');
if (tabs) tabs.style.display = 'flex';
if (table) table.style.display = 'table';
}
function hideTabsAndTable() {
const tabs = document.querySelector('.tabs');
const table = document.querySelector('.content');
if (tabs) tabs.style.display = 'none';
if (table) table.style.display = 'none';
}
function showCompanyOutlook() {
const table = document.querySelector('.content');
table.innerHTML = '';
const rows = [
'Company Name',
'Ticker Symbol',
'Exchange Code',
'Start Date',
'Description'
];
const dataNames = [
"name",
"ticker",
"exchangeCode",
"startDate",
"description"
]
if (window.stockData.companyOutLook) {
rows.forEach((header, i) => {
const row = document.createElement('tr');
const headerCell = document.createElement('td');
headerCell.className = 'content-header';
headerCell.textContent = header;
const dataCell = document.createElement('td');
dataCell.className = 'content-data';
const value = window.stockData.companyOutLook[dataNames[i]] ?? '';
dataCell.textContent = value;
row.appendChild(headerCell);
row.appendChild(dataCell);
table.appendChild(row);
});
}
else {
table.innerHTML = "<tr><td colspan='2'><p>Error: No record has been found, please enter a valid symbol.</p></td></tr>";
}
// Update active tab styling
updateActiveTab(0);
}
function showStockSummary() {
const table = document.querySelector('.content');
table.innerHTML = '';
const rows = [
'Ticker Symbol',
'Trading Day',
'Previous Closing Price',
'Opening Price',
'High Price',
'Low Price',
'Last Price',
'Change',
'Change Percent',
'Number of Shares Traded'
];
if (window.stockData.stockSummary) {
const summary = window.stockData.stockSummary;
const last = Number(summary.last ?? summary.tngoLast ?? 0);
const prevClose = Number(summary.prevClose ?? 0);
const change = prevClose ? last - prevClose : 0;
const changePercent = prevClose ? (change / prevClose) * 100 : 0;
const tradingDay = summary.timestamp
? String(summary.timestamp).split('T')[0]
: '';
const data = [
summary.ticker ?? '',
tradingDay,
prevClose || '',
summary.open ?? '',
summary.high ?? '',
summary.low ?? '',
last || '',
(change > 0 ? "+" + change.toFixed(2) : change < 0 ? change.toFixed(2) : "0") || '',
(changePercent > 0 ? "+" + changePercent.toFixed(2) + "%" : changePercent < 0 ? changePercent.toFixed(2) + "%" : "0%") || '',
summary.volume ?? '',
];
rows.forEach((header, i) => {
const row = document.createElement('tr');
const headerCell = document.createElement('td');
headerCell.className = 'content-header';
headerCell.textContent = header;
const dataCell = document.createElement('td');
dataCell.className = 'content-data';
dataCell.id = `data-${i}`;
dataCell.textContent = data[i];
row.appendChild(headerCell);
row.appendChild(dataCell);
table.appendChild(row);
});
// add image to data 7 and 8
const data7 = document.getElementById('data-7');
const data8 = document.getElementById('data-8');
if (data7 && data7.textContent.includes("+")) {
data7.innerHTML += "<img src='GreenArrowUP.png' alt='Green Arrow Up' width='10' height='10' />"
} else if (data7 && data7.textContent.includes("-")) {
data7.innerHTML += "<img src='RedArrowDown.png' alt='Red Arrow Down' width='10' height='10' />"
}
if (data8 && data8.textContent.includes("+")) {
data8.innerHTML += "<img src='GreenArrowUP.png' alt='Green Arrow Up' width='10' height='10' />"
} else if (data8 && data8.textContent.includes("-")) {
data8.innerHTML += "<img src='RedArrowDown.png' alt='Red Arrow Down' width='10' height='10' />"
}
}
else {
table.innerHTML = "<p>Error: No record has been found, please enter a valid symbol.</p>";
}
// Update active tab styling
updateActiveTab(1);
}
function updateActiveTab(activeIndex) {
const tabs = document.querySelectorAll('.tabs > div');
tabs.forEach((tab, index) => {
if (index === activeIndex) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
}
function showErrorMessage() {
const errorMessage = document.querySelector('.error-message');
errorMessage.style.display = 'block';
}
function hideErrorMessage() {
const errorMessage = document.querySelector('.error-message');
errorMessage.style.display = 'none';
}
// Initialize tabs with click handlers
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.tabs > div');
tabs[0].addEventListener('click', showCompanyOutlook);
tabs[1].addEventListener('click', showStockSummary);
// Search History tab - no action for now
});
function clearInput() {
const inputField = document.getElementById('stockTicker');
inputField.value = '';
window.stockData.companyOutLook = null;
window.stockData.stockSummary = null;
hideTabsAndTable();
hideErrorMessage();
}
</script>
</head>
<body>
<div class="container">
<h1>Stock Search</h1>
<form onsubmit="onBtnClick(event)">
<span>Enter Stock Ticker Symbol<span class="required">*</span></span>
<input type="text" id="stockTicker" required>
<div class="buttons">
<button type="submit" >Search</button>
<button type="button" onclick="clearInput()">Clear</button></div>
</form>
</div>
<div class="divider"></div>
<div class="error-message" style="display: none;">
<p>Error: No record has been found, please enter a valid symbol.</p>
</div>
<div class="tabs" style="display: none;">
<div>Company Outlook</div>
<div>Stock Summary</div>
<div>Search History</div>
</div>
<table class="content" style="display: none;">
<!-- Table rows will be dynamically generated by JavaScript -->
<tbody id="table-body"></tbody>
</table>
</body>
</html>
Here is the app.py file
from flask import Flask, request, jsonify
from flask_cors import CORS
import requests
import sqlite3
app = Flask(__name__)
CORS(app, resources={r"/search*": {"origins": "*"}, r"/add_to_history*": {"origins": "*"}})
API_KEY = "SECRET_API_KEY"
DB_NAME = "search_history.db"
@app.route('/')
def index():
return "Hello, World!"
def init_db():
with sqlite3.connect(DB_NAME) as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS SearchHistory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)''')
# initialize the database
# run init_db() when the app starts
init_db()
@app.route('/history', methods=['GET'])
def get_search_history():
with sqlite3.connect(DB_NAME) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute('SELECT ticker, timestamp FROM SearchHistory LIMIT 10').fetchall()
results = [dict(row) for row in rows]
return jsonify(results)
@app.route('/add_to_history', methods=['POST'])
def add_to_history():
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No JSON data provided'}), 400
ticker = data.get('ticker')
if not ticker:
return jsonify({'error': 'Ticker is required'}), 400
with sqlite3.connect(DB_NAME) as conn:
conn.execute('INSERT INTO SearchHistory(ticker) VALUES (?)', (ticker,))
conn.commit()
return jsonify({'message': 'Record added to history'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
def get_history():
with sqlite3.connect(DB_NAME) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute('SELECT ticker, timestamp FROM SearchHistory LIMIT 10').fetchall()
results = [dict(row) for row in rows]
return results
@app.route('/search', methods=['GET'])
def search():
try:
ticker = request.args.get('ticker', "")
# if not ticker is passed, return error message
if not ticker:
return jsonify({'error': "No record has been found, please enter a valid symbol" })
# define a return object
results = []
# fetch company outlook tab
url = f"https://api.tiingo.com/tiingo/daily/{ticker}?token={API_KEY}"
response = requests.get(url)
data = response.json()
if data and data.get('detail', "") != "Not found.":
results.append(data)
# fetch stock summary tab
url = f"https://api.tiingo.com/iex/{ticker}?token={API_KEY}"
response = requests.get(url)
data = response.json()
if data and len(data) > 0:
results.append(data[0])
# add to history if fetched successfully
# if results:
# add_to_history(ticker)
return jsonify(results)
except Exception as e:
return jsonify({'error': str(e) })
if __name__ == '__main__':
app.run(debug=True)
-
Have you debugged the event handler code for the onClick event? Shot in the blue: At some point in time during the execution an error occured and the greedy catch all block triggers, in the catch block you hide all tabs and change the CSS property style of the error block but you don't actually fill it.Radinator– Radinator2025年12月01日 07:05:26 +00:00Commented Dec 1, 2025 at 7:05
1 Answer 1
Unfortunately, I was unable to reproduce the behavior.
However, the described errors can occur due to various strategies used. I assume that an error occurs within a then block, causing the associated catch block to be executed, but the actual source of the error lies within a called function. Using additional try-catch statements could narrow down the search. I have attempted to eliminate potential sources of error by optimizing the code.
I have made the following changes:
- Data is retrieved using only a single fetch request. A further POST request is unnecessary, as the information is already available.
- Data is stored within DOM elements. Therefore, parts of the display only need to be rebuilt after a fetch request and not through other interactions, such as navigating within the tabs.
from contextlib import closing
from flask import Flask, g, render_template, request, jsonify
import os
import requests
import sqlite3
app = Flask(__name__)
app.config.from_mapping(
API_KEY = "SECRET_API_KEY",
DATABASE = os.path.join(app.instance_path, "search_history.db")
)
def get_db():
db = getattr(g,'_database', None)
if db is None:
db = g._database = sqlite3.connect(app.config['DATABASE'])
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
os.makedirs(app.instance_path, exist_ok=True)
with app.app_context():
db = get_db()
with closing(db.cursor()) as c:
c.execute('''CREATE TABLE IF NOT EXISTS SearchHistory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)''')
db.commit()
@app.route('/')
def index():
return render_template('index.html', **locals())
def fetch_api_data(url):
# It's better to use the token within the header so that
# it isn't displayed within the URL in case of errors.
headers={
'Content-Type': 'application/json',
# 'Authorization': f'Token {app.config['API_KEY']}'
}
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
if details := data.get('details'):
raise Exception(details)
return data
def update_and_select_history(ticker):
db = get_db()
with closing(db.cursor()) as c:
c.execute('INSERT INTO SearchHistory(ticker) VALUES (?)', (ticker,))
c.execute('SELECT ticker, timestamp FROM SearchHistory ORDER BY timestamp DESC LIMIT 10')
results = [dict(row) for row in c.fetchall()]
db.commit()
return results
@app.get('/search')
def search():
ticker = request.args.get('ticker')
# if not ticker is passed, return error message
if not ticker:
return jsonify({'error': "No record has been found, please enter a valid symbol" })
try:
url = f"https://api.tiingo.com/tiingo/daily/{ticker}?token={app.config['API_KEY']}"
outlook = fetch_api_data(url)
outlook = outlook or None
# outlook = {
# "ticker": "AAPL",
# "name": "Apple Inc",
# "exchangeCode": "NASDAQ",
# "startDate": "1980年12月12日",
# "endDate": "2019年01月25日",
# "description": """Apple Inc. (Apple) designs, manufactures and markets mobile communication and ..."""
# }
url = f"https://api.tiingo.com/iex/{ticker}?token={app.config['API_KEY']}"
summary = fetch_api_data(url)
if isinstance(summary, list) and len(summary) > 0:
summary = summary[0]
summary = summary or None
# summary = {
# "ticker":"AAPL",
# "timestamp":"2019年01月30日T10:33:38.186520297-05:00",
# "quoteTimestamp":"2019年01月30日T10:33:38.186520297-05:00",
# "lastSaleTimeStamp":"2019年01月30日T10:33:34.176037579-05:00",
# "last":162.37,
# "lastSize":100,
# "tngoLast":162.33,
# "prevClose":154.68,
# "open":161.83,
# "high":163.25,
# "low":160.38,
# "mid":162.67,
# "volume":0,
# "bidSize":100,
# "bidPrice":162.34,
# "askSize":100,
# "askPrice":163.0
# }
history = update_and_select_history(ticker)
return jsonify(
outlook=outlook,
summary=summary,
history=history
)
except Exception as exc:
print(exc)
return jsonify(error='Error retrieving data.')
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock App</title>
<style>
hr.divider {
border: none;
border-bottom: 1px solid black;
}
.tab {
display: flex;
flex-wrap: wrap;
list-style: none;
border-bottom: 1px solid black;
padding: 0;
}
.tab-item {
display: inline-block;
padding: 0.6rem 0;
padding-right: 1.6rem;
border-bottom: 2px solid transparent;
}
.tab-item.active {
border-bottom: 2px solid black;
}
.tab-pane, .error-message, .results {
display: none;
}
.buttons {
display: inline-block;
}
.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<h1>Stock Search</h1>
<form name="search-form">
<label for="stockTicker">Enter Stock Ticker Symbol<span class="required">*</span></label>
<input type="text" name="ticker" id="stockTicker" required />
<div class="buttons">
<button type="submit">Search</button>
<button type="reset">Clear</button>
</div>
</form>
</div>
<hr class="divider" />
<div class="error-message">
<p>Error: No record has been found, please enter a valid symbol.</p>
</div>
<div class="results">
<div class="tabs">
<ul class="tab">
<li class="tab-item" data-target="tab-outlook">Company Outlook</li>
<li class="tab-item" data-target="tab-summary">Stock Summary</li>
<li class="tab-item" data-target="tab-history">Search History</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tab-outlook"></div>
<div class="tab-pane" id="tab-summary"></div>
<div class="tab-pane" id="tab-history"></div>
</div>
</div>
</div>
<script>
(function() {
const activateElement = (sel, visible) => {
const elem = document.querySelector(sel);
if (visible) {
elem.classList.add('active');
} else {
elem.classList.remove('active');
}
};
const activateTabPane = (target) => {
document.querySelectorAll('.tab-item[data-target]').forEach(item => {
if (item.dataset.target === target) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
document.querySelectorAll('.tab-pane[id]').forEach(tabPaneElem => {
if (tabPaneElem.id !== target) {
tabPaneElem.classList.remove('active');
} else {
tabPaneElem.classList.add('active');
}
});
};
const showCompanyOutlook = (data) => {
const tabElem = document.querySelector('#tab-outlook');
if (!data) {
tabElem && (tabElem.innerHTML = '<p>No data available.</p>');
return;
}
const rowMapping = {
name: 'Company Name',
ticker: 'Ticker Symbol',
exchangeCode: 'Exchange Code',
startDate: 'Start Date',
description: 'Description'
};
const html = '<table class="table-outlook"><tbody>'
+ Object.entries(rowMapping).map(([k,v]) => {
return `
<tr>
<th class="content-header" scope="row">${v}</th>
<td class="content-data">${data[k]}</td>
</tr>
`;
}).join('')
+ '</tbody></table>';
tabElem && (tabElem.innerHTML = html);
};
const showHistory = (data) => {
if (!data) return;
const html = '</ul>' + data.map(row => {
return `<li>${row.ticker} (${row.timestamp})</li>`;
}).join('') + '</ul>';
const historyPane = document.querySelector('#tab-history');
historyPane && (historyPane.innerHTML = html);
};
const showStockSummary = (data) => {
const tabElem = document.querySelector('#tab-summary');
if (!data) {
tabElem && (tabElem.innerHTML = '<p>No data available.</p>');
return;
}
const last = Number(data.last ?? data.tngoLast ?? 0);
const prevClose = Number(data.prevClose ?? 0);
const change = prevClose ? last - prevClose : 0;
const changePercent = prevClose ? (change / prevClose) * 100 : 0;
const tradingDay = data.timestamp
? (new Date(data.timestamp)).toLocaleDateString()
: '';
const dataMapping = {
'Ticker Symbol': data.ticker ?? '',
'Trading Day': tradingDay,
'Previous Closing Price': prevClose || '',
'Opening Price': data.open ?? '',
'High Price': data.high ?? '',
'Low Price': data.low ?? '',
'Last Price': last || '',
'Change': (change > 0 ? "+" + change.toFixed(2) : change < 0 ? change.toFixed(2) : "0") || '',
'Change Percent': `${changePercent < 0 ? '-' : (changePercent > 0 ? '+' : '')}${changePercent.toFixed(2)}%`,
'Number of Shares Traded': data.volume ?? ''
};
const html = '<table class="table-summary"><tbody>'
+ Object.entries(dataMapping).map(([k,v], i) => {
return `
<tr>
<th class="content-header" scope="row">${k}</th>
<td class="content-data" id="data-${i}">${v}</td>
</tr>
`;
}).join('')
+ '</tbody></table>';
tabElem && (tabElem.innerHTML = html);
// ...
};
const form = document.querySelector('form[name="search-form"]');
form.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(this);
const ticker = formData.get('ticker');
const url = `/search?ticker=${encodeURIComponent(ticker)}`;
fetch(url)
.then(resp => {
if (!resp.ok) {
throw new Error(`HTTP-Error: (status=${resp.status})`);
}
return resp.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
const { outlook, summary, history } = data;
showCompanyOutlook(outlook);
showStockSummary(summary);
showHistory(history);
activateElement('.error-message', false);
activateElement('.results', true);
activateTabPane('tab-outlook');
})
.catch(error => {
console.error(error);
activateElement('.error-message', true);
activateElement('.results', false);
});
});
form.addEventListener('reset', () => {
activateElement('.error-message', false);
activateElement('.results', false);
});
const tabItemElems = document.querySelectorAll('.tab-item[data-target]');
tabItemElems.forEach(tabItemElem => {
tabItemElem.addEventListener('click', function() {
activateTabPane(this.dataset.target);
});
});
})();
</script>
</body>
</html>